diff --git a/artifacts/checksums.txt b/artifacts/checksums.txt new file mode 100644 index 00000000..23bf773f --- /dev/null +++ b/artifacts/checksums.txt @@ -0,0 +1,2 @@ +2f392abdc3919f347459b76d7ce3ae3be8fd71e344f56371e5d8c0a4f32bb6dd streamswap_controller.wasm +defca0f7926f953d7d64bd71cc925f98f1faeb767dcc74544571781614238447 streamswap_stream.wasm diff --git a/artifacts/streamswap_controller.wasm b/artifacts/streamswap_controller.wasm new file mode 100644 index 00000000..84a3ecba Binary files /dev/null and b/artifacts/streamswap_controller.wasm differ diff --git a/artifacts/streamswap_stream.wasm b/artifacts/streamswap_stream.wasm new file mode 100644 index 00000000..a6b0880a Binary files /dev/null and b/artifacts/streamswap_stream.wasm differ diff --git a/contracts/stream/src/circuit_ops.rs b/contracts/stream/src/circuit_ops.rs index f4d373ee..9f2bb98f 100644 --- a/contracts/stream/src/circuit_ops.rs +++ b/contracts/stream/src/circuit_ops.rs @@ -1,76 +1,11 @@ -use crate::helpers::build_u128_bank_send_msg; use crate::pool::pool_refund; -use crate::state::{CONTROLLER_PARAMS, POSITIONS, POST_STREAM, STREAM_INFO, STREAM_STATE}; +use crate::state::{CONTROLLER_PARAMS, POST_STREAM, STREAM_INFO, STREAM_STATE}; use crate::stream::{sync_stream, sync_stream_status}; use crate::ContractError; -use cosmwasm_std::{attr, BankMsg, CosmosMsg, DepsMut, Env, MessageInfo, Response, Timestamp}; +use cosmwasm_std::{BankMsg, CosmosMsg, DepsMut, Env, MessageInfo, Response}; +use cw_utils::NativeBalance; use streamswap_types::controller::Params; -use streamswap_types::stream::ThresholdState; -use streamswap_types::stream::{Status, ThresholdError}; - -pub fn execute_exit_cancelled( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { - let mut stream = STREAM_STATE.load(deps.storage)?; - - let mut position = POSITIONS.load(deps.storage, &info.sender)?; - if position.owner != info.sender { - return Err(ContractError::Unauthorized {}); - } - if position.exit_date != Timestamp::from_seconds(0) { - return Err(ContractError::SubscriberAlreadyExited {}); - } - - sync_stream_status(&mut stream, env.block.time); - - // This execution requires the stream to be cancelled or - // the stream to be ended and the threshold not reached. - // If any of other condition fails return not cancelled error. - if !stream.is_cancelled() { - let threshold_state = ThresholdState::new(); - // Threshold should be set - let is_set = threshold_state.check_if_threshold_set(deps.storage)?; - if !is_set { - return Err(ContractError::StreamNotCancelled {}); - } - - // Stream should be ended - if !stream.is_ended() { - return Err(ContractError::StreamNotCancelled {}); - } - // Update stream before checking threshold - sync_stream(&mut stream, env.block.time); - threshold_state.error_if_reached(deps.storage, &stream)?; - } - - // no need to sync position here, we just need to return total balance - let total_balance = position.in_balance + position.spent; - // sync position exit date - position.exit_date = env.block.time; - position.last_updated = env.block.time; - POSITIONS.save(deps.storage, &position.owner, &position)?; - - let send_msg = build_u128_bank_send_msg( - stream.in_denom.clone(), - info.sender.to_string(), - total_balance, - )?; - let attributes = vec![ - attr("action", "exit_cancelled"), - attr("to_address", info.sender.to_string()), - attr("total_balance", total_balance), - attr("exit_date", position.exit_date.to_string()), - attr("last_updated", position.last_updated.to_string()), - ]; - // send funds to the sender - let res = Response::new() - .add_message(send_msg) - .add_attributes(attributes); - - Ok(res) -} +use streamswap_types::stream::Status; pub fn execute_cancel_stream( deps: DepsMut, @@ -96,7 +31,7 @@ pub fn execute_cancel_stream( STREAM_STATE.save(deps.storage, &stream)?; // Refund all out tokens to stream creator(treasury) - let mut refund_coins = vec![stream.out_asset.clone()]; + let mut refund_coins = NativeBalance::default() + stream.out_asset.clone(); // refund pool creation if any let post_stream_ops = POST_STREAM.may_load(deps.storage)?; @@ -106,11 +41,15 @@ pub fn execute_cancel_stream( post_stream_ops.pool_config, stream.out_asset.denom.clone(), )?; - refund_coins.extend(pool_refund_coins); + for coin in pool_refund_coins { + refund_coins += coin; + } } + refund_coins.normalize(); let stream_info = STREAM_INFO.load(deps.storage)?; let funds_msgs: Vec = refund_coins + .into_vec() .iter() .map(|coin| { CosmosMsg::Bank(BankMsg::Send { @@ -125,72 +64,6 @@ pub fn execute_cancel_stream( .add_attribute("status", "cancelled") .add_messages(funds_msgs)) } - -pub fn execute_cancel_stream_with_threshold( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { - let mut stream = STREAM_STATE.load(deps.storage)?; - let stream_info = STREAM_INFO.load(deps.storage)?; - // Only stream creator can cancel the stream with threshold not reached - if info.sender != stream_info.stream_admin { - return Err(ContractError::Unauthorized {}); - } - sync_stream_status(&mut stream, env.block.time); - // Stream should not be cancelled of finalized, should be ended. - // Creator should not able to finalize the stream with threshold not reached but only cancel it. - if !stream.is_ended() { - return Err(ContractError::OperationNotAllowed { - current_status: stream.status_info.status.to_string(), - }); - } - - sync_stream(&mut stream, env.block.time); - - let threshold_state = ThresholdState::new(); - - if !threshold_state.check_if_threshold_set(deps.storage)? { - return Err(ContractError::ThresholdError( - ThresholdError::ThresholdNotSet {}, - )); - } - // Threshold should not be reached - threshold_state.error_if_reached(deps.storage, &stream)?; - - stream.status_info.status = Status::Cancelled; - - STREAM_STATE.save(deps.storage, &stream)?; - - // Refund all out tokens to stream creator(treasury) - let mut refund_coins = vec![stream.out_asset.clone()]; - - // refund pool creation if any - let post_stream_ops = POST_STREAM.may_load(deps.storage)?; - if let Some(post_stream_ops) = post_stream_ops { - let pool_refund_coins = pool_refund( - &deps, - post_stream_ops.pool_config, - stream.out_asset.denom.clone(), - )?; - refund_coins.extend(pool_refund_coins); - } - - let funds_msgs: Vec = refund_coins - .iter() - .map(|coin| { - CosmosMsg::Bank(BankMsg::Send { - to_address: stream_info.treasury.to_string(), - amount: vec![coin.clone()], - }) - }) - .collect(); - - Ok(Response::new() - .add_attribute("action", "cancel_stream") - .add_messages(funds_msgs) - .add_attribute("status", "cancelled")) -} pub fn execute_stream_admin_cancel( deps: DepsMut, env: Env, @@ -216,7 +89,7 @@ pub fn execute_stream_admin_cancel( STREAM_STATE.save(deps.storage, &stream)?; // Refund all out tokens to stream creator(treasury) - let mut refund_coins = vec![stream.out_asset.clone()]; + let mut refund_coins = NativeBalance::default() + stream.out_asset.clone(); // refund pool creation if any let post_stream_ops = POST_STREAM.may_load(deps.storage)?; @@ -226,10 +99,15 @@ pub fn execute_stream_admin_cancel( post_stream_ops.pool_config, stream.out_asset.denom.clone(), )?; - refund_coins.extend(pool_refund_coins); + for coin in pool_refund_coins { + refund_coins += coin; + } } + refund_coins.normalize(); + let funds_msgs: Vec = refund_coins + .into_vec() .iter() .map(|coin| { CosmosMsg::Bank(BankMsg::Send { diff --git a/contracts/stream/src/contract.rs b/contracts/stream/src/contract.rs index 3ddaabb6..44d3a38f 100644 --- a/contracts/stream/src/contract.rs +++ b/contracts/stream/src/contract.rs @@ -1,4 +1,3 @@ -use crate::circuit_ops::execute_cancel_stream_with_threshold; use crate::helpers::{ build_u128_bank_send_msg, check_name_and_url, get_decimals, validate_stream_times, }; @@ -6,22 +5,21 @@ use crate::stream::{compute_shares_amount, sync_stream, sync_stream_status}; use crate::{circuit_ops, ContractError}; use core::str; use cosmwasm_std::{ - attr, entry_point, to_json_binary, Addr, Attribute, BankMsg, Binary, Coin, CosmosMsg, - Decimal256, Deps, DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult, Timestamp, - Uint128, Uint256, + attr, entry_point, to_json_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Decimal256, Deps, + DepsMut, Env, MessageInfo, Order, Response, StdResult, Timestamp, Uint128, Uint256, }; use cw2::{ensure_from_older_version, set_contract_version}; use cw_storage_plus::Bound; -use cw_utils::{maybe_addr, must_pay}; +use cw_utils::{maybe_addr, must_pay, NativeBalance}; use std::env; use streamswap_types::stream::{ - AveragePriceResponse, ExecuteMsg, LatestStreamedPriceResponse, PositionResponse, - PositionsResponse, QueryMsg, StreamResponse, + AveragePriceResponse, ExecuteMsg, FinalizedStatus, LatestStreamedPriceResponse, + PositionResponse, PositionsResponse, QueryMsg, StreamResponse, }; -use streamswap_types::stream::{PostStreamActions, StreamInfo, StreamState, ThresholdState}; +use streamswap_types::stream::{PostStreamActions, StreamInfo, StreamState}; use streamswap_utils::to_uint256; -use crate::pool::pool_operations; +use crate::pool::{pool_operations, pool_refund}; use crate::state::{ CONTROLLER_PARAMS, CREATOR_VESTING, POSITIONS, POST_STREAM, STREAM_INFO, STREAM_STATE, SUBSCRIBER_VESTING, TOS, TOS_SIGNED, @@ -80,6 +78,11 @@ pub fn instantiate( if in_denom == out_asset.denom { return Err(ContractError::SameDenomOnEachSide {}); } + if let Some(threshold) = threshold { + if threshold.is_zero() { + return Err(ContractError::InvalidThreshold {}); + } + } let stream_admin = deps.api.addr_validate(&stream_admin)?; let treasury = deps.api.addr_validate(&treasury)?; @@ -92,6 +95,7 @@ pub fn instantiate( bootstraping_start_time, start_time, end_time, + threshold, ); STREAM_STATE.save(deps.storage, &stream_state)?; @@ -102,9 +106,6 @@ pub fn instantiate( PostStreamActions::new(pool_config.clone(), subscriber_vesting, creator_vesting); POST_STREAM.save(deps.storage, &post_stream_actions)?; - let threshold_state = ThresholdState::new(); - threshold_state.set_threshold_if_any(threshold, deps.storage)?; - TOS.save(deps.storage, &tos_version)?; let mut attrs = vec![ @@ -164,10 +165,6 @@ pub fn execute( } => execute_finalize_stream(deps, env, info, new_treasury, create_pool, salt), ExecuteMsg::ExitStream { salt } => execute_exit_stream(deps, env, info, salt), ExecuteMsg::CancelStream {} => circuit_ops::execute_cancel_stream(deps, env, info), - ExecuteMsg::ExitCancelled {} => circuit_ops::execute_exit_cancelled(deps, env, info), - ExecuteMsg::CancelStreamWithThreshold {} => { - execute_cancel_stream_with_threshold(deps, env, info) - } ExecuteMsg::StreamAdminCancel {} => { circuit_ops::execute_stream_admin_cancel(deps, env, info) } @@ -303,7 +300,7 @@ pub fn execute_subscribe( if !(stream_state.is_active() || stream_state.is_bootstrapping()) { return Err(ContractError::OperationNotAllowed { - current_status: stream_state.status_info.status.to_string(), + current_status: stream_state.status_info.status.clone().to_string(), }); } @@ -446,229 +443,297 @@ pub fn execute_finalize_stream( salt: Option, ) -> Result { let stream_info = STREAM_INFO.load(deps.storage)?; + let controller_params = CONTROLLER_PARAMS.load(deps.storage)?; if stream_info.stream_admin != info.sender { return Err(ContractError::Unauthorized {}); } let mut stream_state = STREAM_STATE.load(deps.storage)?; - sync_stream_status(&mut stream_state, env.block.time); - - if stream_state.is_finalized() || stream_state.is_cancelled() || !stream_state.is_ended() { - return Err(ContractError::OperationNotAllowed { - current_status: stream_state.status_info.status.to_string(), - }); - } sync_stream(&mut stream_state, env.block.time); - - stream_state.status_info.status = Status::Finalized; - - // If threshold is set and not reached, finalize will fail - // Creator should execute cancel_stream_with_threshold to cancel the stream - // Only returns error if threshold is set and not reached - let thresholds_state = ThresholdState::new(); - thresholds_state.error_if_not_reached(deps.storage, &stream_state)?; - - STREAM_STATE.save(deps.storage, &stream_state)?; - - let controller_params = CONTROLLER_PARAMS.load(deps.storage)?; + sync_stream_status(&mut stream_state, env.block.time); let treasury = maybe_addr(deps.api, new_treasury)?.unwrap_or_else(|| stream_info.treasury.clone()); - let mut messages = vec![]; - let mut attributes = vec![]; - - // last creator revenue = spent_in - swap_fee - in_clp; - let mut creator_revenue = stream_state.spent_in; + match ( + stream_state.status_info.clone().status, + stream_state.check_threshold(), + ) { + (Status::Ended, true) => { + let mut messages = vec![]; + let mut attributes = vec![]; + + // creator revenue = spent_in - swap_fee - in_clp; + let mut creator_revenue = stream_state.spent_in; + + // Stream's swap fee collected at fixed rate from accumulated spent_in of positions(ie stream.spent_in) + let swap_fee = Decimal256::from_ratio(stream_state.spent_in, Uint128::one()) + .checked_mul(controller_params.exit_fee_percent)? + * Uint256::one(); + + // extract swap_fee from last amount + creator_revenue = creator_revenue.checked_sub(swap_fee)?; + let creator_revenue_u128 = Uint128::try_from(creator_revenue)?; + + // In case the stream is ended without any shares in it. We need to refund the remaining + // out tokens although that is unlikely to happen. + if stream_state.out_remaining > Uint256::zero() { + let remaining_out = stream_state.out_remaining; + let uint128_remaining_out = Uint128::try_from(remaining_out)?; + // Sub remaining out tokens from out_asset + stream_state.out_asset.amount = stream_state + .out_asset + .amount + .checked_sub(uint128_remaining_out)?; + let remaining_msg = CosmosMsg::Bank(BankMsg::Send { + to_address: treasury.to_string(), + amount: vec![Coin { + denom: stream_state.out_asset.denom.clone(), + amount: uint128_remaining_out, + }], + }); + messages.push(remaining_msg); + } - // Stream's swap fee collected at fixed rate from accumulated spent_in of positions(ie stream.spent_in) - let swap_fee = Decimal256::from_ratio(stream_state.spent_in, Uint128::one()) - .checked_mul(controller_params.exit_fee_percent)? - * Uint256::one(); + // if vesting is not set we need to send the creator revenue to the treasury in outer scope + let mut vesting_flag = false; + // execute post stream actions + let post_stream_actions = POST_STREAM.may_load(deps.storage)?; + if let Some(post_stream_actions) = post_stream_actions { + // if pool config and create pool is set, create a pool for the stream + creator_revenue = match (post_stream_actions.pool_config, create_pool) { + (Some(pool_config), Some(create_pool)) => { + let (msgs, attrs, creator_revenue) = pool_operations( + &deps, + create_pool, + env.contract.address.clone(), + stream_state.in_denom.clone(), + stream_state.out_asset.denom.clone(), + stream_state.out_asset.amount, + creator_revenue, + pool_config, + )?; + messages.extend(msgs); + attributes.extend(attrs); + Ok(creator_revenue) + } + (None, None) => Ok(creator_revenue), + _ => Err(ContractError::InvalidPoolConfig {}), + }?; + + // if subscriber vesting is set, instantiate a vested release contract for user and send + if let Some(creator_vesting) = post_stream_actions.creator_vesting { + let vesting_checksum = deps + .querier + .query_wasm_code_info(controller_params.vesting_code_id)? + .checksum; + let (vesting_msgs, vesting_attributes, vesting_addr) = vesting_operations( + &deps, + env.contract.address, + vesting_checksum, + treasury.clone(), + salt, + stream_state.status_info.end_time, + controller_params.vesting_code_id, + creator_revenue_u128, + stream_state.in_denom.clone(), + creator_vesting, + )?; + messages.extend(vesting_msgs); + attributes.extend(vesting_attributes); + CREATOR_VESTING.save(deps.storage, &vesting_addr)?; + vesting_flag = true; + } + } - // extract swap_fee from last amount - creator_revenue = creator_revenue.checked_sub(swap_fee)?; - let creator_revenue_u128 = Uint128::try_from(creator_revenue)?; - - // In case the stream is ended without any shares in it. We need to refund the remaining - // out tokens although that is unlikely to happen. - if stream_state.out_remaining > Uint256::zero() { - let remaining_out = stream_state.out_remaining; - let uint128_remaining_out = Uint128::try_from(remaining_out)?; - // Sub remaining out tokens from out_asset - stream_state.out_asset.amount = stream_state - .out_asset - .amount - .checked_sub(uint128_remaining_out)?; - let remaining_msg = CosmosMsg::Bank(BankMsg::Send { - to_address: treasury.to_string(), - amount: vec![Coin { - denom: stream_state.out_asset.denom.clone(), - amount: uint128_remaining_out, - }], - }); - messages.push(remaining_msg); - } + if !vesting_flag { + let send_msg = CosmosMsg::Bank(BankMsg::Send { + to_address: treasury.to_string(), + amount: vec![Coin { + denom: stream_state.in_denom.clone(), + amount: creator_revenue_u128, + }], + }); + messages.push(send_msg); + } - // if vesting is not set we need to send the creator revenue to the treasury in outer scope - let mut vesting_flag = false; - // execute post stream actions - let post_stream_actions = POST_STREAM.may_load(deps.storage)?; - if let Some(post_stream_actions) = post_stream_actions { - // if pool config and create pool is set, create a pool for the stream - creator_revenue = match (post_stream_actions.pool_config, create_pool) { - (Some(pool_config), Some(create_pool)) => { - let (msgs, attrs, creator_revenue) = pool_operations( + let swap_fee_msg = build_u128_bank_send_msg( + stream_state.in_denom.clone(), + controller_params.fee_collector.to_string(), + swap_fee, + )?; + messages.push(swap_fee_msg); + + stream_state.status_info.status = Status::Finalized(FinalizedStatus::ThresholdReached); + STREAM_STATE.save(deps.storage, &stream_state)?; + + attributes.extend(vec![ + attr("action", "finalize_stream"), + attr("treasury", treasury.to_string()), + attr("fee_collector", controller_params.fee_collector.to_string()), + attr("creators_revenue", creator_revenue), + attr( + "refunded_out_remaining", + stream_state.out_remaining.to_string(), + ), + attr( + "total_sold", + to_uint256(stream_state.out_asset.amount) + .checked_sub(stream_state.out_remaining)? + .to_string(), + ), + attr("swap_fee", swap_fee), + attr( + "creation_fee_amount", + controller_params.stream_creation_fee.amount.to_string(), + ), + ]); + + Ok(Response::new() + .add_messages(messages) + .add_attributes(attributes)) + } + (Status::Ended, false) => { + // if stream is ended and threshold is not reached, return all in tokens to treasury + // Refund all out tokens to stream creator(treasury) + let mut refund_coins = NativeBalance::default() + stream_state.out_asset.clone(); + + // refund pool creation if any + let post_stream_ops = POST_STREAM.may_load(deps.storage)?; + if let Some(post_stream_ops) = post_stream_ops { + let pool_refund_coins = pool_refund( &deps, - create_pool, - env.contract.address.clone(), - stream_state.in_denom.clone(), + post_stream_ops.pool_config, stream_state.out_asset.denom.clone(), - stream_state.out_asset.amount, - creator_revenue, - pool_config, )?; - messages.extend(msgs); - attributes.extend(attrs); - Ok(creator_revenue) + for coin in pool_refund_coins { + refund_coins += coin; + } } - (None, None) => Ok(creator_revenue), - _ => Err(ContractError::InvalidPoolConfig {}), - }?; - - // if subscriber vesting is set, instantiate a vested release contract for user and send - if let Some(creator_vesting) = post_stream_actions.creator_vesting { - let vesting_checksum = deps - .querier - .query_wasm_code_info(controller_params.vesting_code_id)? - .checksum; - let (vesting_msgs, vesting_attributes, vesting_addr) = vesting_operations( - &deps, - env.contract.address, - vesting_checksum, - treasury.clone(), - salt, - stream_state.status_info.end_time, - controller_params.vesting_code_id, - creator_revenue_u128, - stream_state.in_denom.clone(), - creator_vesting, - )?; - messages.extend(vesting_msgs); - attributes.extend(vesting_attributes); - CREATOR_VESTING.save(deps.storage, &vesting_addr)?; - vesting_flag = true; + refund_coins.normalize(); + + let funds_msgs: Vec = refund_coins + .into_vec() + .iter() + .map(|coin| { + CosmosMsg::Bank(BankMsg::Send { + to_address: stream_info.treasury.to_string(), + amount: vec![coin.clone()], + }) + }) + .collect(); + + stream_state.status_info.status = + Status::Finalized(FinalizedStatus::ThresholdNotReached); + STREAM_STATE.save(deps.storage, &stream_state)?; + + Ok(Response::new() + .add_attribute("action", "finalize_stream") + .add_attribute("status", "threshold_not_reached") + .add_attribute("treasury", stream_info.treasury.to_string()) + .add_messages(funds_msgs)) } + _ => Err(ContractError::OperationNotAllowed { + current_status: stream_state.status_info.status.to_string(), + }), } - - if !vesting_flag { - let send_msg = CosmosMsg::Bank(BankMsg::Send { - to_address: treasury.to_string(), - amount: vec![Coin { - denom: stream_state.in_denom.clone(), - amount: creator_revenue_u128, - }], - }); - messages.push(send_msg); - } - - let swap_fee_msg = build_u128_bank_send_msg( - stream_state.in_denom.clone(), - controller_params.fee_collector.to_string(), - swap_fee, - )?; - messages.push(swap_fee_msg); - - attributes.extend(vec![ - attr("action", "finalize_stream"), - attr("treasury", treasury.to_string()), - attr("fee_collector", controller_params.fee_collector.to_string()), - attr("creators_revenue", creator_revenue), - attr( - "refunded_out_remaining", - stream_state.out_remaining.to_string(), - ), - attr( - "total_sold", - to_uint256(stream_state.out_asset.amount) - .checked_sub(stream_state.out_remaining)? - .to_string(), - ), - attr("swap_fee", swap_fee), - attr( - "creation_fee_amount", - controller_params.stream_creation_fee.amount.to_string(), - ), - ]); - - Ok(Response::new() - .add_messages(messages) - .add_attributes(attributes)) } - pub fn execute_exit_stream( deps: DepsMut, env: Env, info: MessageInfo, salt: Option, ) -> Result { + // Load stream state and controller parameters let mut stream_state = STREAM_STATE.load(deps.storage)?; let controller_params = CONTROLLER_PARAMS.load(deps.storage)?; - // check if stream is paused - sync_stream_status(&mut stream_state, env.block.time); - if stream_state.is_cancelled() || !(stream_state.is_ended() || stream_state.is_finalized()) { - return Err(ContractError::OperationNotAllowed { - current_status: stream_state.status_info.status.to_string(), - }); + // Load the position for the sender + let mut position = POSITIONS.load(deps.storage, &info.sender)?; + + // Ensure the sender has not already exited + if position.exit_date != Timestamp::from_seconds(0) { + return Err(ContractError::SubscriberAlreadyExited {}); } + // Sync stream status and position + sync_stream_status(&mut stream_state, env.block.time); sync_stream(&mut stream_state, env.block.time); - let threshold_state = ThresholdState::new(); + // Match on stream status and threshold to determine exit behavior + match ( + stream_state.status_info.clone().status, + stream_state.check_threshold(), + ) { + // Normal exit scenario: stream is ended and threshold is reached + (Status::Ended, true) | (Status::Finalized(FinalizedStatus::ThresholdReached), _) => { + handle_normal_exit( + deps, + env, + info, + salt, + &mut stream_state, + &controller_params, + &mut position, + ) + } - threshold_state.error_if_not_reached(deps.storage, &stream_state)?; + // Full refund exit scenario: stream ended with threshold not reached or cancelled + (Status::Ended, false) + | (Status::Finalized(FinalizedStatus::ThresholdNotReached), _) + | (Status::Cancelled, _) => { + handle_full_refund_exit(deps, env, info, &mut stream_state, &mut position) + } - let mut position = POSITIONS.load(deps.storage, &info.sender)?; - if position.exit_date != Timestamp::from_seconds(0) { - return Err(ContractError::SubscriberAlreadyExited {}); + // Error case: operation not allowed + _ => Err(ContractError::OperationNotAllowed { + current_status: stream_state.status_info.status.to_string(), + }), } +} - // sync position before exit +/// Handles the normal exit scenario where the stream has ended and the threshold is reached +fn handle_normal_exit( + deps: DepsMut, + env: Env, + info: MessageInfo, + salt: Option, + stream_state: &mut StreamState, + controller_params: &ControllerParams, + position: &mut Position, +) -> Result { sync_position( stream_state.dist_index, stream_state.shares, stream_state.status_info.last_updated, stream_state.in_supply, - &mut position, + position, )?; - stream_state.shares = stream_state.shares.checked_sub(position.shares)?; - STREAM_STATE.save(deps.storage, &stream_state)?; - // sync position exit date + // Update stream shares and position exit date + stream_state.shares = stream_state.shares.checked_sub(position.shares)?; + STREAM_STATE.save(deps.storage, stream_state)?; position.exit_date = env.block.time; - POSITIONS.save(deps.storage, &position.owner, &position)?; + POSITIONS.save(deps.storage, &position.owner, position)?; - // Swap fee = fixed_rate*position.spent_in this calculation is only for execution reply attributes + // Calculate exit fee let swap_fee = Decimal256::from_ratio(position.spent, Uint128::one()) .checked_mul(controller_params.exit_fee_percent)? * Uint256::one(); - let mut messages: Vec = vec![]; - let mut attributes: Vec = vec![]; + // Prepare messages and attributes for the response + let mut messages = vec![]; + let mut attributes = vec![]; - // if vesting is set, instantiate a vested release contract for user and send - // the out tokens to the contract let uint128_purchased = Uint128::try_from(position.purchased)?; - let mut vesting_flag = false; - let post_stream_actions = POST_STREAM.may_load(deps.storage)?; - if let Some(post_stream_actions) = post_stream_actions { + if let Some(post_stream_actions) = POST_STREAM.may_load(deps.storage)? { if let Some(vesting_config) = post_stream_actions.subscriber_vesting { let vesting_checksum = deps .querier .query_wasm_code_info(controller_params.vesting_code_id)? .checksum; + let (vesting_msgs, vesting_attributes, vesting_addr) = vesting_operations( &deps, env.contract.address, @@ -688,7 +753,6 @@ pub fn execute_exit_stream( } } - // if vesting is not set we need to send the out tokens to the user if !vesting_flag { let send_msg = CosmosMsg::Bank(BankMsg::Send { to_address: info.sender.to_string(), @@ -700,14 +764,13 @@ pub fn execute_exit_stream( messages.push(send_msg); } - // if there is any unspent in balance, send it back to the user if !position.in_balance.is_zero() { let unspent = position.in_balance; let uint128_unspent = Uint128::try_from(unspent)?; let unspent_msg = CosmosMsg::Bank(BankMsg::Send { to_address: info.sender.to_string(), amount: vec![Coin { - denom: stream_state.in_denom, + denom: stream_state.in_denom.clone(), amount: uint128_unspent, }], }); @@ -716,6 +779,7 @@ pub fn execute_exit_stream( attributes.extend(vec![ attr("action", "exit_stream"), + attr("status_info", stream_state.status_info.status.to_string()), attr("spent", position.spent.checked_sub(swap_fee)?), attr("purchased", position.purchased), attr("swap_fee_paid", swap_fee), @@ -726,6 +790,37 @@ pub fn execute_exit_stream( .add_attributes(attributes)) } +/// Handles the full refund exit scenario, where the stream is cancelled or threshold is not reached +fn handle_full_refund_exit( + deps: DepsMut, + env: Env, + info: MessageInfo, + stream_state: &mut StreamState, + position: &mut Position, +) -> Result { + // Calculate total balance and update exit date + let total_balance = position.in_balance + position.spent; + position.exit_date = env.block.time; + position.last_updated = env.block.time; + POSITIONS.save(deps.storage, &position.owner, position)?; + + let send_msg = build_u128_bank_send_msg( + stream_state.in_denom.clone(), + info.sender.to_string(), + total_balance, + )?; + let attributes = vec![ + attr("action", "exit_stream"), + attr("total_balance", total_balance), + attr("status_info", stream_state.status_info.status.to_string()), + ]; + + // Return the funds to the sender + Ok(Response::new() + .add_message(send_msg) + .add_attributes(attributes)) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; @@ -743,7 +838,6 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { } QueryMsg::AveragePrice {} => to_json_binary(&query_average_price(deps, env)?), QueryMsg::LastStreamedPrice {} => to_json_binary(&query_last_streamed_price(deps, env)?), - QueryMsg::Threshold {} => to_json_binary(&query_threshold_state(deps, env)?), QueryMsg::ToS { addr } => { if let Some(addr) = addr { to_json_binary(&query_tos_signed( @@ -754,6 +848,19 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { to_json_binary(&query_tos(deps)?) } } + QueryMsg::CreatorVesting {} => { + let creator_vesting = CREATOR_VESTING + .load(deps.storage) + .unwrap_or(Addr::unchecked("".to_string())); + to_json_binary(&creator_vesting) + } + QueryMsg::SubscriberVesting { addr } => { + let addr = deps.api.addr_validate(&addr)?; + let subscriber_vesting = SUBSCRIBER_VESTING + .load(deps.storage, addr) + .unwrap_or(Addr::unchecked("".to_string())); + to_json_binary(&subscriber_vesting) + } } } pub fn query_params(deps: Deps) -> StdResult { @@ -847,12 +954,6 @@ pub fn query_last_streamed_price(deps: Deps, _env: Env) -> StdResult Result, StdError> { - let threshold_state = ThresholdState::new(); - let threshold = threshold_state.get_threshold(deps.storage)?; - Ok(threshold) -} - pub fn query_tos(deps: Deps) -> StdResult { let tos = TOS.load(deps.storage)?; Ok(tos) diff --git a/contracts/stream/src/error.rs b/contracts/stream/src/error.rs index 978a3a0e..e0a2ed9f 100644 --- a/contracts/stream/src/error.rs +++ b/contracts/stream/src/error.rs @@ -7,8 +7,6 @@ use std::convert::Infallible; use streamswap_utils::payment_checker::CustomPaymentError; use thiserror::Error; -use streamswap_types::stream::ThresholdError; - #[derive(Error, Debug, PartialEq)] pub enum ContractError { #[error("{0}")] @@ -26,9 +24,6 @@ pub enum ContractError { #[error("{0}")] DivideByZeroError(#[from] DivideByZeroError), - #[error("{0}")] - ThresholdError(#[from] ThresholdError), - #[error("{0}")] CustomPayment(#[from] CustomPaymentError), @@ -156,4 +151,10 @@ pub enum ContractError { #[error("Invalid pool config")] InvalidPoolConfig {}, + + #[error("Threshold must be greater than zero")] + InvalidThreshold {}, + + #[error("Vesting contract not found")] + VestingContractNotFound {}, } diff --git a/contracts/stream/src/stream.rs b/contracts/stream/src/stream.rs index 5206e170..d7914eb4 100644 --- a/contracts/stream/src/stream.rs +++ b/contracts/stream/src/stream.rs @@ -1,11 +1,13 @@ use cosmwasm_std::{Decimal, Decimal256, Fraction, Timestamp, Uint256}; use std::ops::Mul; -use streamswap_types::stream::{Status, StreamState}; +use streamswap_types::stream::{FinalizedStatus, Status, StreamState}; pub fn sync_stream_status(stream: &mut StreamState, now: Timestamp) { if matches!( stream.status_info.status, - Status::Finalized | Status::Cancelled + Status::Cancelled + | Status::Finalized(FinalizedStatus::ThresholdNotReached) + | Status::Finalized(FinalizedStatus::ThresholdReached) ) { return; } diff --git a/packages/types/src/stream/error.rs b/packages/types/src/stream/error.rs index 5d980873..860cf00a 100644 --- a/packages/types/src/stream/error.rs +++ b/packages/types/src/stream/error.rs @@ -1,20 +1,20 @@ -use cosmwasm_std::StdError; -use thiserror::Error; +// use cosmwasm_std::StdError; +// use thiserror::Error; -#[derive(Error, Debug, PartialEq)] -pub enum ThresholdError { - #[error(transparent)] - Std(#[from] StdError), +// #[derive(Error, Debug, PartialEq)] +// pub enum ThresholdError { +// #[error(transparent)] +// Std(#[from] StdError), - #[error("Threshold not reached")] - ThresholdNotReached {}, +// #[error("Threshold not reached")] +// ThresholdNotReached {}, - #[error("Threshold reached")] - ThresholdReached {}, +// #[error("Threshold reached")] +// ThresholdReached {}, - #[error("Threshold not set")] - ThresholdNotSet {}, +// #[error("Threshold not set")] +// ThresholdNotSet {}, - #[error("Min price can't be zero")] - ThresholdZero {}, -} +// #[error("Min price can't be zero")] +// ThresholdZero {}, +// } diff --git a/packages/types/src/stream/mod.rs b/packages/types/src/stream/mod.rs index c1c8b534..55bb467e 100644 --- a/packages/types/src/stream/mod.rs +++ b/packages/types/src/stream/mod.rs @@ -2,10 +2,6 @@ mod error; mod msg; mod position; mod stream; -mod threshold; - -pub use error::*; pub use msg::*; pub use position::*; pub use stream::*; -pub use threshold::*; diff --git a/packages/types/src/stream/msg.rs b/packages/types/src/stream/msg.rs index a9271608..d6c4d4f5 100644 --- a/packages/types/src/stream/msg.rs +++ b/packages/types/src/stream/msg.rs @@ -33,11 +33,7 @@ pub enum ExecuteMsg { }, // // Circuit features - // - /// ExitCancelled returns the whole balance user put in the stream, both spent and unspent. - ExitCancelled {}, CancelStream {}, - CancelStreamWithThreshold {}, StreamAdminCancel {}, } @@ -72,10 +68,15 @@ pub enum QueryMsg { /// Returns currently streaming price of a sale. #[returns(LatestStreamedPriceResponse)] LastStreamedPrice {}, - #[returns(Uint128)] - Threshold {}, + /// Returns the terms of service. #[returns(String)] ToS { addr: Option }, + /// Returns the instantiated vesting contract for the creator. + #[returns(String)] + CreatorVesting {}, + /// Returns the instantiated contract contract of the subscriber. + #[returns(String)] + SubscriberVesting { addr: String }, } #[cw_serde] diff --git a/packages/types/src/stream/stream.rs b/packages/types/src/stream/stream.rs index a960d736..b0b01b56 100644 --- a/packages/types/src/stream/stream.rs +++ b/packages/types/src/stream/stream.rs @@ -25,6 +25,8 @@ pub struct StreamState { pub out_asset: Coin, /// Status info of the stream pub status_info: StatusInfo, + /// Threshold amount of the stream + pub threshold: Option, } impl StreamState { @@ -35,6 +37,7 @@ impl StreamState { bootstrapping_start_time: Timestamp, start_time: Timestamp, end_time: Timestamp, + threshold: Option, ) -> Self { StreamState { dist_index: Decimal256::zero(), @@ -46,6 +49,7 @@ impl StreamState { shares: Uint256::zero(), current_streamed_price: Decimal256::zero(), status_info: StatusInfo::new(now, bootstrapping_start_time, start_time, end_time), + threshold, } } @@ -54,7 +58,8 @@ impl StreamState { } pub fn is_finalized(&self) -> bool { - self.status_info.status == Status::Finalized + self.status_info.status == Status::Finalized(FinalizedStatus::ThresholdReached) + || self.status_info.status == Status::Finalized(FinalizedStatus::ThresholdNotReached) } pub fn is_waiting(&self) -> bool { @@ -72,6 +77,12 @@ impl StreamState { pub fn is_ended(&self) -> bool { self.status_info.status == Status::Ended } + pub fn check_threshold(&self) -> bool { + match self.threshold { + Some(threshold) => self.spent_in >= threshold, + None => true, + } + } } #[cw_serde] @@ -126,21 +137,39 @@ pub enum Status { /// Waiting status is when the stream is created. In this status, no one can interact with the stream. Waiting, /// Bootstrapping status is when the stream is bootstrapping. - /// In this status, subscriber and withdraw are permitted. But no spending is allowed on each side + /// In this status, subscriber and withdraw are permitted. But no spending is allowed on each side. Bootstrapping, /// Active status is when the stream is active. In this status, spending is allowed on each side. Active, /// Ended status is when the stream is ended. /// In this status, Subscriber can exit the stream, creator can finalize and collect accumulated in assets. Ended, - /// Finalized status is when the stream is finalized. In this status, Subscriber can exit the stream. - Finalized, + /// Finalized status indicates the stream has reached its final state. This can be one of two outcomes: + Finalized(FinalizedStatus), /// Cancelled status is when the stream is cancelled. /// In this status, Subscriber can exit the stream and collect full in assets. /// Creator can collect full out assets. Cancelled, } +/// Represents whether the stream's threshold was reached or not in the finalized state. +#[cw_serde] +pub enum FinalizedStatus { + /// Indicates that the stream's threshold was reached. + ThresholdReached, + /// Indicates that the stream's threshold was not reached. + ThresholdNotReached, +} + +impl std::fmt::Display for FinalizedStatus { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + FinalizedStatus::ThresholdReached => write!(f, "ThresholdReached"), + FinalizedStatus::ThresholdNotReached => write!(f, "ThresholdNotReached"), + } + } +} + impl std::fmt::Display for Status { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { @@ -148,8 +177,8 @@ impl std::fmt::Display for Status { Status::Bootstrapping => write!(f, "Bootstrapping"), Status::Active => write!(f, "Active"), Status::Ended => write!(f, "Ended"), - Status::Finalized => write!(f, "Finalized"), Status::Cancelled => write!(f, "Cancelled"), + Status::Finalized(finalized_status) => write!(f, "Finalized({})", finalized_status), } } } diff --git a/packages/types/src/stream/threshold.rs b/packages/types/src/stream/threshold.rs deleted file mode 100644 index 7e3a63e1..00000000 --- a/packages/types/src/stream/threshold.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::stream::{StreamState, ThresholdError}; -use cosmwasm_std::{StdError, Storage, Uint256}; -use cw_storage_plus::Item; - -pub type Threshold = Uint256; - -pub const THRESHOLDS_STATE_KEY: &str = "thresholds"; - -pub struct ThresholdState<'a>(Item<'a, Threshold>); - -impl<'a> ThresholdState<'a> { - pub fn new() -> Self { - ThresholdState(Item::new(THRESHOLDS_STATE_KEY)) - } - - pub fn set_threshold_if_any( - &self, - threshold: Option, - storage: &mut dyn Storage, - ) -> Result<(), ThresholdError> { - match threshold { - Some(threshold) => { - if threshold.is_zero() { - return Err(ThresholdError::ThresholdZero {}); - } - self.0.save(storage, &threshold)?; - Ok(()) - } - None => Ok(()), - } - } - pub fn error_if_not_reached( - &self, - storage: &dyn Storage, - state: &StreamState, - ) -> Result<(), ThresholdError> { - // If threshold is not set, It returns ok - // If threshold is set, It returns error if threshold is not reached - let threshold = self.0.may_load(storage)?; - if let Some(threshold) = threshold { - if state.spent_in < threshold { - Err(ThresholdError::ThresholdNotReached {}) - } else { - Ok(()) - } - } else { - Ok(()) - } - } - - pub fn error_if_reached( - &self, - storage: &dyn Storage, - state: &StreamState, - ) -> Result<(), ThresholdError> { - let threshold = self.0.may_load(storage)?; - if let Some(threshold) = threshold { - if state.spent_in >= threshold { - Err(ThresholdError::ThresholdReached {}) - } else { - Ok(()) - } - } else { - Ok(()) - } - } - pub fn check_if_threshold_set(&self, storage: &dyn Storage) -> Result { - let threshold = self.0.may_load(storage)?; - Ok(threshold.is_some()) - } - pub fn get_threshold(&self, storage: &dyn Storage) -> Result, StdError> { - let threshold = self.0.may_load(storage)?; - Ok(threshold) - } -} - -impl<'a> Default for ThresholdState<'a> { - fn default() -> Self { - ThresholdState::new() - } -} diff --git a/tests/src/tests/streamswap_tests/create_stream.rs b/tests/src/tests/streamswap_tests/create_stream.rs index 37cd0b26..6a19d18f 100644 --- a/tests/src/tests/streamswap_tests/create_stream.rs +++ b/tests/src/tests/streamswap_tests/create_stream.rs @@ -11,7 +11,6 @@ mod create_stream_tests { use streamswap_stream::ContractError as StreamSwapError; use streamswap_types::controller::Params as ControllerParams; use streamswap_types::controller::QueryMsg; - use streamswap_types::stream::ThresholdError; use streamswap_utils::payment_checker::CustomPaymentError; #[test] @@ -396,10 +395,7 @@ mod create_stream_tests { let err = res.source().unwrap().source().unwrap(); let error = err.downcast_ref::().unwrap(); - assert_eq!( - *error, - StreamSwapError::ThresholdError(ThresholdError::ThresholdZero {}) - ); + assert_eq!(*error, StreamSwapError::InvalidThreshold {}); } #[test] diff --git a/tests/src/tests/streamswap_tests/exit_cancel.rs b/tests/src/tests/streamswap_tests/exit_cancel.rs deleted file mode 100644 index 930b0697..00000000 --- a/tests/src/tests/streamswap_tests/exit_cancel.rs +++ /dev/null @@ -1,201 +0,0 @@ -#[cfg(test)] -mod exit_cancel { - use crate::helpers::mock_messages::CreateStreamMsgBuilder; - use crate::helpers::suite::SuiteBuilder; - use crate::helpers::utils::get_wasm_attribute_with_key; - use crate::helpers::{ - mock_messages::get_controller_inst_msg, - suite::Suite, - utils::{get_contract_address_from_res, get_funds_from_res}, - }; - use cosmwasm_std::{coin, Addr, BlockInfo, Uint256}; - use cw_multi_test::Executor; - use streamswap_stream::ContractError; - use streamswap_types::stream::ExecuteMsg as StreamSwapExecuteMsg; - - #[test] - fn exit_without_stream_cancelled() { - 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 create_stream_msg = CreateStreamMsgBuilder::new( - "stream", - test_accounts.creator_1.as_ref(), - coin(100, "out_denom"), - "in_denom", - bootstrapping_start_time, - start_time, - end_time, - ) - .build(); - - let _res = app - .execute_contract( - test_accounts.creator_1.clone(), - controller_address.clone(), - &create_stream_msg, - &[coin(100, "fee_denom"), coin(100, "out_denom")], - ) - .unwrap(); - - let stream_swap_contract_address = get_contract_address_from_res(_res); - - app.set_block(BlockInfo { - time: start_time.plus_seconds(20), - height: 2, - chain_id: "test".to_string(), - }); - - let subscribe_msg = StreamSwapExecuteMsg::Subscribe {}; - - let _res = app - .execute_contract( - test_accounts.subscriber_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &subscribe_msg, - &[coin(10, "in_denom")], - ) - .unwrap(); - - let exit_msg = StreamSwapExecuteMsg::ExitCancelled {}; - - let res = app - .execute_contract( - test_accounts.subscriber_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &exit_msg, - &[], - ) - .unwrap_err(); - - let err = res.source().unwrap(); - let error = err.downcast_ref::().unwrap(); - assert_eq!(error, &ContractError::StreamNotCancelled {}); - } - - #[test] - fn exit_cancel_happy_path() { - 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 create_stream_msg = CreateStreamMsgBuilder::new( - "stream", - test_accounts.creator_1.as_ref(), - coin(100, "out_denom"), - "in_denom", - bootstrapping_start_time, - start_time, - end_time, - ) - .threshold(Uint256::from(100u128)) - .build(); - - let _res = app - .execute_contract( - test_accounts.creator_1.clone(), - controller_address.clone(), - &create_stream_msg, - &[coin(100, "fee_denom"), coin(100, "out_denom")], - ) - .unwrap(); - - let stream_swap_contract_address = get_contract_address_from_res(_res); - - app.set_block(BlockInfo { - time: start_time.plus_seconds(20), - height: 2, - chain_id: "test".to_string(), - }); - - let subscribe_msg = StreamSwapExecuteMsg::Subscribe {}; - - let subscribe_amount = coin(10, "in_denom"); - let _res = app - .execute_contract( - test_accounts.subscriber_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &subscribe_msg, - &[subscribe_amount.clone()], - ) - .unwrap(); - - app.execute_contract( - test_accounts.admin.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &StreamSwapExecuteMsg::CancelStream {}, - &[], - ) - .unwrap(); - - let exit_msg = StreamSwapExecuteMsg::ExitCancelled {}; - - let res = app - .execute_contract( - test_accounts.subscriber_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &exit_msg, - &[], - ) - .unwrap(); - - let recipient = get_wasm_attribute_with_key(res.clone(), "to_address".to_string()); - assert_eq!(recipient, test_accounts.subscriber_1.to_string()); - - let last_updated = get_wasm_attribute_with_key(res.clone(), "last_updated".to_string()); - assert_eq!(last_updated, app.block_info().time.to_string()); - - let exit_date = get_wasm_attribute_with_key(res.clone(), "exit_date".to_string()); - assert_eq!(exit_date, app.block_info().time.to_string()); - - let funds = get_funds_from_res(res.clone()); - assert_eq!( - funds, - vec![( - test_accounts.subscriber_1.to_string(), - subscribe_amount.clone() - )] - ); - } -} diff --git a/tests/src/tests/streamswap_tests/exit_stream.rs b/tests/src/tests/streamswap_tests/exit_stream.rs new file mode 100644 index 00000000..84b16a3d --- /dev/null +++ b/tests/src/tests/streamswap_tests/exit_stream.rs @@ -0,0 +1,561 @@ +#[cfg(test)] +mod exit_stream { + use crate::helpers::mock_messages::CreateStreamMsgBuilder; + use crate::helpers::suite::SuiteBuilder; + use crate::helpers::{ + mock_messages::get_controller_inst_msg, + suite::Suite, + utils::{get_contract_address_from_res, get_funds_from_res}, + }; + use cosmwasm_std::{coin, Addr, BlockInfo, Uint128, Uint256}; + use cw_multi_test::Executor; + use streamswap_stream::ContractError; + use streamswap_types::stream::ExecuteMsg as StreamSwapExecuteMsg; + + #[test] + fn happy_path() { + 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 create_stream_msg = CreateStreamMsgBuilder::new( + "stream", + test_accounts.creator_1.as_ref(), + coin(100, "out_denom"), + "in_denom", + bootstrapping_start_time, + start_time, + end_time, + ) + .build(); + + let _res = app + .execute_contract( + test_accounts.creator_1.clone(), + controller_address.clone(), + &create_stream_msg, + &[coin(100, "fee_denom"), coin(100, "out_denom")], + ) + .unwrap(); + + let stream_swap_contract_address = get_contract_address_from_res(_res); + + app.set_block(BlockInfo { + time: start_time.plus_seconds(20), + height: 2, + chain_id: "test".to_string(), + }); + + let subscribe_msg = StreamSwapExecuteMsg::Subscribe {}; + + let _res = app + .execute_contract( + test_accounts.subscriber_1.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &subscribe_msg, + &[coin(10, "in_denom")], + ) + .unwrap(); + + app.set_block(BlockInfo { + time: end_time.plus_seconds(20), + height: 3, + chain_id: "test".to_string(), + }); + + let exit_msg = StreamSwapExecuteMsg::ExitStream { salt: None }; + + let res = app + .execute_contract( + test_accounts.subscriber_1.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &exit_msg, + &[], + ) + .unwrap(); + + let funds = get_funds_from_res(res); + + assert_eq!(funds[0].1.amount, Uint128::from(100u128)); + assert_eq!(funds[0].1.denom, "out_denom"); + assert_eq!(funds.len(), 1); + } + + #[test] + fn exit_stream_threshold_not_reached() { + 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 threshold = Uint256::from(1000u128); + + let create_stream_msg = CreateStreamMsgBuilder::new( + "stream", + test_accounts.creator_1.as_ref(), + coin(100, "out_denom"), + "in_denom", + bootstrapping_start_time, + start_time, + end_time, + ) + .threshold(threshold) + .build(); + + let _res = app + .execute_contract( + test_accounts.creator_1.clone(), + controller_address.clone(), + &create_stream_msg, + &[coin(100, "fee_denom"), coin(100, "out_denom")], + ) + .unwrap(); + + let stream_swap_contract_address = get_contract_address_from_res(_res); + + app.set_block(BlockInfo { + time: start_time.plus_seconds(20), + height: 2, + chain_id: "test".to_string(), + }); + + // Subscriber 1 subscribes to the stream + let subscribe_msg = StreamSwapExecuteMsg::Subscribe {}; + + let _res = app + .execute_contract( + test_accounts.subscriber_1.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &subscribe_msg, + &[coin(10, "in_denom")], + ) + .unwrap(); + + // Subscriber 2 subscribes to the stream + let _res = app + .execute_contract( + test_accounts.subscriber_2.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &subscribe_msg, + &[coin(10, "in_denom")], + ) + .unwrap(); + + app.set_block(BlockInfo { + time: end_time.plus_seconds(20), + height: 3, + chain_id: "test".to_string(), + }); + + // Subscriber 1 exits the stream before its finalized but the threshold is not reached + let exit_msg = StreamSwapExecuteMsg::ExitStream { salt: None }; + + let res = app + .execute_contract( + test_accounts.subscriber_1.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &exit_msg, + &[], + ) + .unwrap(); + + let funds = get_funds_from_res(res); + + assert_eq!(funds[0].1.amount, Uint128::from(10u128)); + assert_eq!(funds[0].1.denom, "in_denom"); + assert_eq!(funds[0].0, test_accounts.subscriber_1); + + // Creator finalizes the stream after subscriber 1 exits + let finalize_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()), + &finalize_msg, + &[], + ) + .unwrap(); + // Creator gets full refund + let funds = get_funds_from_res(res); + let expected = [( + test_accounts.creator_1.clone().into_string(), + coin(100, "out_denom"), + )]; + + assert_eq!(funds, expected); + + // Subscriber 2 exits the stream after its finalized + let res = app + .execute_contract( + test_accounts.subscriber_2.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &exit_msg, + &[], + ) + .unwrap(); + + let funds = get_funds_from_res(res); + + assert_eq!(funds[0].1.amount, Uint128::from(10u128)); + assert_eq!(funds[0].1.denom, "in_denom"); + + // Check balance of the stream contract + // The stream contract should have no balance + let balance = app + .wrap() + .query_all_balances(stream_swap_contract_address) + .unwrap(); + + assert_eq!(balance.len(), 0); + } + + #[test] + fn exit_stream_cancelled() { + 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 create_stream_msg = CreateStreamMsgBuilder::new( + "stream", + test_accounts.creator_1.as_ref(), + coin(100, "out_denom"), + "in_denom", + bootstrapping_start_time, + start_time, + end_time, + ) + .build(); + + let _res = app + .execute_contract( + test_accounts.creator_1.clone(), + controller_address.clone(), + &create_stream_msg, + &[coin(100, "fee_denom"), coin(100, "out_denom")], + ) + .unwrap(); + + let stream_swap_contract_address = get_contract_address_from_res(_res); + + app.set_block(BlockInfo { + time: start_time.plus_seconds(20), + height: 2, + chain_id: "test".to_string(), + }); + + // Subscriber 1 subscribes to the stream + let subscribe_msg = StreamSwapExecuteMsg::Subscribe {}; + + let _res = app + .execute_contract( + test_accounts.subscriber_1.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &subscribe_msg, + &[coin(10, "in_denom")], + ) + .unwrap(); + + // Subscriber 2 subscribes to the stream + let _res = app + .execute_contract( + test_accounts.subscriber_2.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &subscribe_msg, + &[coin(10, "in_denom")], + ) + .unwrap(); + + // Protocol admin cancels the stream + let cancel_msg = StreamSwapExecuteMsg::CancelStream {}; + + let res = app + .execute_contract( + test_accounts.admin.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &cancel_msg, + &[], + ) + .unwrap(); + // Creator gets full refund + let funds = get_funds_from_res(res); + let expected = [( + test_accounts.creator_1.clone().into_string(), + coin(100, "out_denom"), + )]; + + assert_eq!(funds, expected); + + // Subscriber 1 exits the stream after its cancelled + let exit_msg = StreamSwapExecuteMsg::ExitStream { salt: None }; + + let res = app + .execute_contract( + test_accounts.subscriber_1.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &exit_msg, + &[], + ) + .unwrap(); + + let funds = get_funds_from_res(res); + + assert_eq!(funds[0].1.amount, Uint128::from(10u128)); + assert_eq!(funds[0].1.denom, "in_denom"); + assert_eq!(funds[0].0, test_accounts.subscriber_1); + + // Check balance of the stream contract + // The stream contract should still have the remaining balance from subscriber 2 + let balance = app + .wrap() + .query_all_balances(stream_swap_contract_address.clone()) + .unwrap(); + + assert_eq!(balance.len(), 1); + + // Subscriber 2 exits the stream after its cancelled + let res = app + .execute_contract( + test_accounts.subscriber_2.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &exit_msg, + &[], + ) + .unwrap(); + + let funds = get_funds_from_res(res); + + assert_eq!(funds[0].1.amount, Uint128::from(10u128)); + assert_eq!(funds[0].1.denom, "in_denom"); + + // Check balance of the stream contract + // The stream contract should have no balance + let balance = app + .wrap() + .query_all_balances(stream_swap_contract_address) + .unwrap(); + + assert_eq!(balance.len(), 0); + } + + #[test] + fn exit_without_a_position() { + 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(); + + 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); + + // Attempting to exit without a position + let exit_msg = StreamSwapExecuteMsg::ExitStream { salt: None }; + + let _err = app + .execute_contract( + test_accounts.subscriber_2.clone(), // No position for this subscriber + Addr::unchecked(stream_swap_contract_address.clone()), + &exit_msg, + &[], + ) + .unwrap_err(); + } + + #[test] + fn attempt_exit_after_already_exited() { + 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); + + // Instantiate the controller + 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(); + + // Create a stream + 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(); + + 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); + + // Simulate block time to allow subscription + app.set_block(BlockInfo { + height: 1_100, + time: start_time, + chain_id: "test".to_string(), + }); + + // Subscriber subscribes to the stream + let subscribe_msg = StreamSwapExecuteMsg::Subscribe {}; + app.execute_contract( + test_accounts.subscriber_1.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &subscribe_msg, + &[coin(150, "in_denom")], + ) + .unwrap(); + + // Simulate block time for stream execution + app.set_block(BlockInfo { + height: 1_200, + time: end_time.plus_seconds(1), + chain_id: "test".to_string(), + }); + + // Subscriber exits the stream + let exit_msg = StreamSwapExecuteMsg::ExitStream { salt: None }; + app.execute_contract( + test_accounts.subscriber_1.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &exit_msg, + &[], + ) + .unwrap(); + + // Attempt to exit again + let err = app + .execute_contract( + test_accounts.subscriber_1.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &exit_msg, + &[], + ) + .unwrap_err(); + + assert_eq!( + err.downcast::().unwrap(), + ContractError::SubscriberAlreadyExited {} + ); + } +} diff --git a/tests/src/tests/streamswap_tests/finalize_stream.rs b/tests/src/tests/streamswap_tests/finalize_stream.rs index f42fa43f..47a4ff79 100644 --- a/tests/src/tests/streamswap_tests/finalize_stream.rs +++ b/tests/src/tests/streamswap_tests/finalize_stream.rs @@ -1,13 +1,16 @@ #[cfg(test)] mod finalize_stream_tests { + use std::str::FromStr; + use crate::helpers::mock_messages::CreateStreamMsgBuilder; use crate::helpers::suite::SuiteBuilder; use crate::helpers::utils::{get_contract_address_from_res, get_funds_from_res}; use crate::helpers::{mock_messages::get_controller_inst_msg, suite::Suite}; - use cosmwasm_std::{coin, Addr, BlockInfo, Coin, Uint128}; + use cosmwasm_std::{coin, Addr, BlockInfo, Coin, Uint128, Uint256}; use cw_multi_test::Executor; use streamswap_types::stream::{ - ExecuteMsg as StreamSwapExecuteMsg, QueryMsg as StreamSwapQueryMsg, Status, StreamResponse, + ExecuteMsg as StreamSwapExecuteMsg, FinalizedStatus, QueryMsg as StreamSwapQueryMsg, + Status, StreamResponse, }; #[test] @@ -113,7 +116,11 @@ mod finalize_stream_tests { &StreamSwapQueryMsg::Stream {}, ) .unwrap(); - assert_eq!(stream.status, Status::Finalized); + + assert_eq!( + stream.status, + Status::Finalized(FinalizedStatus::ThresholdReached) + ); // Creator_1 can finalize the stream only once let _res = app .execute_contract( @@ -239,7 +246,10 @@ mod finalize_stream_tests { ) .unwrap(); - assert_eq!(stream.status, Status::Finalized); + assert_eq!( + stream.status, + Status::Finalized(FinalizedStatus::ThresholdReached) + ); } #[test] @@ -368,6 +378,278 @@ mod finalize_stream_tests { ) .unwrap(); - assert_eq!(stream.status, Status::Finalized); + assert_eq!( + stream.status, + Status::Finalized(FinalizedStatus::ThresholdReached) + ); + } + + #[test] + fn finalize_stream_threshold_not_reached() { + 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 threshold = Uint256::from(1_000u128); + 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, + ) + .threshold(threshold) + .build(); + 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, + chain_id: "test".to_string(), + }); + + // Finalize stream with threshold not reached + 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(1_000_000) + } + ),] + ); + assert_eq!(stream_swap_funds.len(), 1); + } + + #[test] + fn finalize_stream_threshold_not_reached_pool_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 threshold = Uint256::from(1_000u128); + let pool_creation_fee: Coin = coin(1000000, "fee_denom"); + let pool_out_amount_clp: Uint256 = Uint256::from(500_000u128); + 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, + ) + .threshold(threshold) + .pool_config( + streamswap_types::controller::PoolConfig::ConcentratedLiquidity { + out_amount_clp: pool_out_amount_clp, + }, + ) + .build(); + + 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"), + pool_creation_fee.clone(), + coin( + Uint128::from_str(pool_out_amount_clp.to_string().as_str()) + .unwrap() + .u128(), + "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, + chain_id: "test".to_string(), + }); + + // Finalize stream with threshold not reached + 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()), + pool_creation_fee.clone() + ), + ( + String::from(test_accounts.creator_1.clone()), + Coin { + denom: "out_denom".to_string(), + amount: Uint128::new(1_000_000 + 500_000) + } + ), + ] + ); + // Contract only has the subscription amount left in the contract + let contract_balance = app + .wrap() + .query_all_balances(Addr::unchecked(stream_swap_contract_address.clone())) + .unwrap(); + assert_eq!(contract_balance.len(), 1); + assert_eq!( + contract_balance[0], + Coin { + denom: "in_denom".to_string(), + amount: Uint128::new(200) + } + ); + + // Query the stream status(Check stream status) + let stream: StreamResponse = app + .wrap() + .query_wasm_smart( + Addr::unchecked(stream_swap_contract_address.clone()), + &StreamSwapQueryMsg::Stream {}, + ) + .unwrap(); + + assert_eq!( + stream.status, + Status::Finalized(FinalizedStatus::ThresholdNotReached) + ); + + // Subscriber 1 exits the stream + let exit_stream_msg = StreamSwapExecuteMsg::ExitStream { salt: None }; + + let res = app + .execute_contract( + test_accounts.subscriber_1.clone(), + Addr::unchecked(stream_swap_contract_address.clone()), + &exit_stream_msg, + &[], + ) + .unwrap(); + let stream_swap_funds = get_funds_from_res(res); + assert_eq!( + stream_swap_funds, + vec![( + String::from(test_accounts.subscriber_1.clone()), + Coin { + denom: "in_denom".to_string(), + amount: Uint128::new(200) + } + ),] + ); + + // Contract has no funds left + let contract_balance = app + .wrap() + .query_all_balances(Addr::unchecked(stream_swap_contract_address.clone())) + .unwrap(); + assert_eq!(contract_balance.len(), 0); } } diff --git a/tests/src/tests/streamswap_tests/mod.rs b/tests/src/tests/streamswap_tests/mod.rs index 96969fd2..8de5aa60 100644 --- a/tests/src/tests/streamswap_tests/mod.rs +++ b/tests/src/tests/streamswap_tests/mod.rs @@ -1,6 +1,6 @@ mod cancel_stream; mod create_stream; -mod exit_cancel; +mod exit_stream; mod finalize_stream; mod pool; mod rounding_leftover; diff --git a/tests/src/tests/streamswap_tests/pool.rs b/tests/src/tests/streamswap_tests/pool.rs index cdd86e23..9f26481e 100644 --- a/tests/src/tests/streamswap_tests/pool.rs +++ b/tests/src/tests/streamswap_tests/pool.rs @@ -7,6 +7,7 @@ mod pool { }; use cosmwasm_std::{coin, Addr, BlockInfo, Coin, Uint256}; use cw_multi_test::Executor; + use cw_utils::NativeBalance; use streamswap_types::controller::{CreatePool, PoolConfig}; use streamswap_types::stream::ExecuteMsg as StreamSwapExecuteMsg; use streamswap_types::stream::QueryMsg as StreamSwapQueryMsg; @@ -243,17 +244,22 @@ mod pool { let res_pool_amount = coin(out_clp_amount, out_denom); let res_refund_out_amount = coin(out_supply, out_denom); - assert_eq!( - fund_transfer, - vec![ - (test_accounts.creator_1.to_string(), res_refund_out_amount), - (test_accounts.creator_1.to_string(), pool_creation_fee), - (test_accounts.creator_1.to_string(), res_pool_amount), - ] - ); + let mut expected: NativeBalance = NativeBalance::default() + + res_pool_amount.clone() + + pool_creation_fee.clone() + + res_refund_out_amount.clone(); + expected.normalize(); + + let expected_res: Vec<(String, Coin)> = expected + .into_vec() + .iter() + .map(|coin| (test_accounts.creator_1.to_string(), coin.clone())) + .collect(); + + assert_eq!(fund_transfer, expected_res); } #[test] - fn cancel_stream_with_threshold_pool_clp_refund() { + fn finalize_stream_threshold_not_reached() { let Suite { mut app, test_accounts, @@ -343,37 +349,38 @@ mod pool { chain_id: "test".to_string(), }); - // Try finalizing stream should fail as threshold is not met + // Finalize stream let finalize_stream_msg = StreamSwapExecuteMsg::FinalizeStream { new_treasury: None, create_pool: None, salt: None, }; - let _err = app - .execute_contract( - test_accounts.creator_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &finalize_stream_msg, - &[], - ) - .unwrap_err(); - // Threshold cancel stream - let cancel_stream_msg = StreamSwapExecuteMsg::CancelStreamWithThreshold {}; let res = app .execute_contract( test_accounts.creator_1.clone(), Addr::unchecked(stream_swap_contract_address.clone()), - &cancel_stream_msg, + &finalize_stream_msg, &[], ) .unwrap(); let res_funds = get_funds_from_res(res.clone()); + let mut expected: NativeBalance = NativeBalance::default() + + out_coin.clone() + + pool_creation_fee.clone() + + pool_out_coin.clone(); + expected.normalize(); + assert_eq!( res_funds, vec![ - (test_accounts.creator_1.to_string(), out_coin), - (test_accounts.creator_1.to_string(), pool_creation_fee), - (test_accounts.creator_1.to_string(), pool_out_coin), + ( + test_accounts.creator_1.to_string(), + expected.clone().into_vec()[0].clone() + ), + ( + test_accounts.creator_1.to_string(), + expected.into_vec()[1].clone() + ), ] ); } @@ -466,13 +473,17 @@ mod pool { ) .unwrap(); let res_funds = get_funds_from_res(res.clone()); - assert_eq!( - res_funds, - vec![ - (test_accounts.creator_1.to_string(), out_coin), - (test_accounts.creator_1.to_string(), pool_creation_fee), - (test_accounts.creator_1.to_string(), pool_out_coin), - ] - ); + let mut expected_res_funds: NativeBalance = NativeBalance::default() + + out_coin.clone() + + pool_creation_fee.clone() + + pool_out_coin.clone(); + expected_res_funds.normalize(); + + let expected_res: Vec<(String, Coin)> = expected_res_funds + .into_vec() + .iter() + .map(|coin| (test_accounts.creator_1.to_string(), coin.clone())) + .collect(); + assert_eq!(res_funds, expected_res); } } diff --git a/tests/src/tests/streamswap_tests/shares.rs b/tests/src/tests/streamswap_tests/shares.rs index 95953b41..8137eb5d 100644 --- a/tests/src/tests/streamswap_tests/shares.rs +++ b/tests/src/tests/streamswap_tests/shares.rs @@ -16,6 +16,7 @@ mod shares { Timestamp::from_seconds(0), Timestamp::from_seconds(100), Timestamp::from_seconds(0), + None, ); // add new shares diff --git a/tests/src/tests/streamswap_tests/threshold.rs b/tests/src/tests/streamswap_tests/threshold.rs index 2635986b..5edf65d5 100644 --- a/tests/src/tests/streamswap_tests/threshold.rs +++ b/tests/src/tests/streamswap_tests/threshold.rs @@ -1,545 +1,545 @@ -#[cfg(test)] -mod threshold { - - use crate::helpers::mock_messages::CreateStreamMsgBuilder; - use crate::helpers::suite::SuiteBuilder; - use crate::helpers::utils::{get_contract_address_from_res, get_funds_from_res}; - use crate::helpers::{mock_messages::get_controller_inst_msg, suite::Suite}; - use cosmwasm_std::testing::MockStorage; - use cosmwasm_std::{coin, Addr, BlockInfo, Coin, Timestamp, Uint128, Uint256}; - use cw_multi_test::Executor; - use streamswap_stream::ContractError as StreamSwapError; - use streamswap_types::stream::{ - ExecuteMsg as StreamSwapExecuteMsg, QueryMsg as StreamSwapQueryMsg, StreamResponse, - StreamState, ThresholdError, - }; - use streamswap_types::stream::{Status, ThresholdState}; - - #[test] - fn thresholds_state() { - let mut storage = MockStorage::new(); - let thresholds = ThresholdState::new(); - let mut stream = StreamState::new( - Timestamp::from_seconds(0), - Coin { - denom: "out_denom".to_string(), - amount: Uint128::from(100u128), - }, - "in_denom".to_string(), - Timestamp::from_seconds(0), - Timestamp::from_seconds(100), - Timestamp::from_seconds(0), - ); - let threshold = Uint256::from(1_500_000_000_000u128); - - thresholds - .set_threshold_if_any(Some(threshold), &mut storage) - .unwrap(); - - stream.spent_in = Uint256::from(1_500_000_000_000u128 - 1u128); - let result = thresholds.error_if_not_reached(&storage, &stream.clone()); - assert!(result.is_err()); - stream.spent_in = Uint256::from(1_500_000_000_000u128); - let result = thresholds.error_if_not_reached(&storage, &stream.clone()); - assert!(result.is_ok()); - } - #[test] - fn threshold_reached() { - 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(1_000_000); - let end_time = app.block_info().time.plus_seconds(5_000_000); - let bootstrapping_start_time = app.block_info().time.plus_seconds(500_000); - let threshold = Uint256::from(250u128); - - 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(500, "out_denom"), - "in_denom", - bootstrapping_start_time, - start_time, - end_time, - ) - .url("https://sample.url".to_string()) - .threshold(threshold) - .build(); - - let res = app - .execute_contract( - test_accounts.creator_1.clone(), - controller_address.clone(), - &create_stream_msg, - &[coin(100, "fee_denom"), coin(500, "out_denom")], - ) - .unwrap(); - let stream_swap_contract_address: String = get_contract_address_from_res(res); - - let subscribe_msg = StreamSwapExecuteMsg::Subscribe {}; - // Set time to start of the stream - app.set_block(BlockInfo { - time: start_time, - height: 1_000, - chain_id: "test".to_string(), - }); - - let _res = app - .execute_contract( - test_accounts.subscriber_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &subscribe_msg, - &[coin(500, "in_denom")], - ) - .unwrap(); - - // Set block to the end of the stream - app.set_block(BlockInfo { - time: end_time.plus_seconds(1), - height: 1_000, - chain_id: "test".to_string(), - }); - - // Threshold should be reached - let exit_msg = StreamSwapExecuteMsg::ExitStream { salt: None }; - - let res = app - .execute_contract( - test_accounts.subscriber_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &exit_msg, - &[], - ) - .unwrap(); - - // Exit should be possible - // Since there is only one subscriber all out denom should be sent to subscriber - let funds = get_funds_from_res(res); - assert_eq!( - Uint256::from(funds[0].1.amount.u128()), - Uint256::from(500u128) - ); - assert_eq!(funds[0].1.denom, "out_denom".to_string()); - - // Creator finalizes the stream - let finalize_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()), - &finalize_msg, - &[], - ) - .unwrap(); - - // Creator's revenue - let funds = get_funds_from_res(res); - assert_eq!( - Uint256::from(funds[0].1.amount.u128()), - Uint256::from(495u128) - ); - assert_eq!(funds[0].1.denom, "in_denom".to_string()); - // Fee collector's revenue - assert_eq!( - Uint256::from(funds[1].1.amount.u128()), - Uint256::from(5u128) - ); - assert_eq!(funds[1].1.denom, "in_denom".to_string()); - } - - #[test] - fn threshold_not_reached() { - 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(1_000_000); - let end_time = app.block_info().time.plus_seconds(5_000_000); - let bootstrapping_start_time = app.block_info().time.plus_seconds(500_000); - let threshold = Uint256::from(500u128); - - 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(500, "out_denom"), - "in_denom", - bootstrapping_start_time, - start_time, - end_time, - ) - .url("https://sample.url".to_string()) - .threshold(threshold) - .build(); - - let res = app - .execute_contract( - test_accounts.creator_1.clone(), - controller_address.clone(), - &create_stream_msg, - &[coin(100, "fee_denom"), coin(500, "out_denom")], - ) - .unwrap(); - let stream_swap_contract_address: String = get_contract_address_from_res(res); - - // Set time to start of the stream - app.set_block(BlockInfo { - time: start_time, - height: 1_000, - chain_id: "test".to_string(), - }); - - let subscribe_msg = StreamSwapExecuteMsg::Subscribe {}; - // Subscription 1 - let _res = app - .execute_contract( - test_accounts.subscriber_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &subscribe_msg, - &[coin(250, "in_denom")], - ) - .unwrap(); - // Subscription 2 - let _res = app - .execute_contract( - test_accounts.subscriber_2.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &subscribe_msg, - &[coin(1, "in_denom")], - ) - .unwrap(); - - // Set block to the end of the stream - app.set_block(BlockInfo { - time: end_time.plus_seconds(1), - height: 1_000, - chain_id: "test".to_string(), - }); - - // Exit should not be possible - let exit_msg = StreamSwapExecuteMsg::ExitStream { salt: None }; - - let _res = app - .execute_contract( - test_accounts.subscriber_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &exit_msg, - &[], - ) - .unwrap_err(); - - // Finalize should not be possible - let finalize_msg = StreamSwapExecuteMsg::FinalizeStream { - new_treasury: None, - create_pool: None, - salt: None, - }; - - let err = app - .execute_contract( - test_accounts.creator_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &finalize_msg, - &[], - ) - .unwrap_err(); - let error = err.downcast::().unwrap(); - assert_eq!( - error, - StreamSwapError::ThresholdError(ThresholdError::ThresholdNotReached {}) - ); - - // Subscriber one executes exit cancelled before creator cancels stream - let exit_cancelled_msg = StreamSwapExecuteMsg::ExitCancelled {}; - // Subscriber 1 executes exit cancelled - let res = app - .execute_contract( - test_accounts.subscriber_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &exit_cancelled_msg, - &[], - ) - .unwrap(); - let subscriber_1_funds = get_funds_from_res(res); - assert_eq!(subscriber_1_funds.len(), 1); - assert_eq!( - Uint256::from(subscriber_1_funds[0].1.amount.u128()), - Uint256::from(250u128) - ); - assert_eq!(subscriber_1_funds[0].1.denom, "in_denom".to_string()); - - // Creator threshold cancels the stream - let cancel_msg = StreamSwapExecuteMsg::CancelStreamWithThreshold {}; - - let res = app - .execute_contract( - test_accounts.creator_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &cancel_msg, - &[], - ) - .unwrap(); - let creator_funds = get_funds_from_res(res); - assert_eq!(creator_funds.len(), 1); - assert_eq!( - Uint256::from(creator_funds[0].1.amount.u128()), - Uint256::from(500u128) - ); - assert_eq!(creator_funds[0].1.denom, "out_denom".to_string()); - - // Creator can not finalize the stream - let finalize_msg = StreamSwapExecuteMsg::FinalizeStream { - new_treasury: None, - create_pool: None, - salt: None, - }; - - let err = app - .execute_contract( - test_accounts.creator_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &finalize_msg, - &[], - ) - .unwrap_err(); - let error = err.downcast::().unwrap(); - assert_eq!( - error, - StreamSwapError::OperationNotAllowed { - current_status: "Cancelled".to_string() - } - ); - - // Creator can not cancel the stream again - let cancel_msg = StreamSwapExecuteMsg::CancelStreamWithThreshold {}; - - let err = app - .execute_contract( - test_accounts.creator_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &cancel_msg, - &[], - ) - .unwrap_err(); - let error = err.downcast::().unwrap(); - assert_eq!( - error, - StreamSwapError::OperationNotAllowed { - current_status: "Cancelled".to_string() - } - ); - - // Subscriber 2 executes exit cancelled after creator cancels stream - let exit_cancelled_msg = StreamSwapExecuteMsg::ExitCancelled {}; - - let res = app - .execute_contract( - test_accounts.subscriber_2.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &exit_cancelled_msg, - &[], - ) - .unwrap(); - let subscriber_2_funds = get_funds_from_res(res); - assert_eq!(subscriber_2_funds.len(), 1); - assert_eq!( - Uint256::from(subscriber_2_funds[0].1.amount.u128()), - Uint256::from(1u128) - ); - assert_eq!(subscriber_2_funds[0].1.denom, "in_denom".to_string()); - - // Query stream should return stream with is_cancelled = true - let query_msg = StreamSwapQueryMsg::Stream {}; - - let res: StreamResponse = app - .wrap() - .query_wasm_smart( - Addr::unchecked(stream_swap_contract_address.clone()), - &query_msg, - ) - .unwrap(); - - assert_eq!(res.status, Status::Cancelled); - } - - #[test] - fn threshold_cancel() { - 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(1_000_000); - let end_time = app.block_info().time.plus_seconds(5_000_000); - let bootstrapping_start_time = app.block_info().time.plus_seconds(500_000); - let threshold = Uint256::from(500u128); - - 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(500, "out_denom"), - "in_denom", - bootstrapping_start_time, - start_time, - end_time, - ) - .url("https://sample.url".to_string()) - .threshold(threshold) - .build(); - - let res = app - .execute_contract( - test_accounts.creator_1.clone(), - controller_address.clone(), - &create_stream_msg, - &[coin(100, "fee_denom"), coin(500, "out_denom")], - ) - .unwrap(); - let stream_swap_contract_address: String = get_contract_address_from_res(res); - // Set time to start of the stream - app.set_block(BlockInfo { - time: start_time, - height: 1_000, - chain_id: "test".to_string(), - }); - - // Subscription 1 - let subscribe_msg = StreamSwapExecuteMsg::Subscribe {}; - - let _res = app - .execute_contract( - test_accounts.subscriber_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &subscribe_msg, - &[coin(250, "in_denom")], - ) - .unwrap(); - - // Subscription 2 - let _res = app - .execute_contract( - test_accounts.subscriber_2.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &subscribe_msg, - &[coin(250 - 1, "in_denom")], - ) - .unwrap(); - - // Can not cancel stream before it ends - let cancel_msg = StreamSwapExecuteMsg::CancelStreamWithThreshold {}; - - let err = app - .execute_contract( - test_accounts.creator_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &cancel_msg, - &[], - ) - .unwrap_err(); - let error = err.downcast::().unwrap(); - assert_eq!( - error, - StreamSwapError::OperationNotAllowed { - current_status: "Active".to_string() - } - ); - - // Set block to the end of the stream - app.set_block(BlockInfo { - time: end_time.plus_seconds(1), - height: 1_000, - chain_id: "test".to_string(), - }); - - // Non creator can't cancel stream - let cancel_msg = StreamSwapExecuteMsg::CancelStreamWithThreshold {}; - - let err = app - .execute_contract( - test_accounts.subscriber_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &cancel_msg, - &[], - ) - .unwrap_err(); - - let error = err.downcast::().unwrap(); - - assert_eq!(error, StreamSwapError::Unauthorized {}); - - // Creator can cancel stream - let cancel_msg = StreamSwapExecuteMsg::CancelStreamWithThreshold {}; - - let _res = app - .execute_contract( - test_accounts.creator_1.clone(), - Addr::unchecked(stream_swap_contract_address.clone()), - &cancel_msg, - &[], - ) - .unwrap(); - - // Query stream should return stream with is_cancelled = true - - let query_msg = StreamSwapQueryMsg::Stream {}; - - let res: StreamResponse = app - .wrap() - .query_wasm_smart( - Addr::unchecked(stream_swap_contract_address.clone()), - &query_msg, - ) - .unwrap(); - - assert_eq!(res.status, Status::Cancelled); - } -} +// #[cfg(test)] +// mod threshold { + +// use crate::helpers::mock_messages::CreateStreamMsgBuilder; +// use crate::helpers::suite::SuiteBuilder; +// use crate::helpers::utils::{get_contract_address_from_res, get_funds_from_res}; +// use crate::helpers::{mock_messages::get_controller_inst_msg, suite::Suite}; +// use cosmwasm_std::testing::MockStorage; +// use cosmwasm_std::{coin, Addr, BlockInfo, Coin, Timestamp, Uint128, Uint256}; +// use cw_multi_test::Executor; +// use streamswap_stream::ContractError as StreamSwapError; +// use streamswap_types::stream::{ +// ExecuteMsg as StreamSwapExecuteMsg, QueryMsg as StreamSwapQueryMsg, StreamResponse, +// StreamState, ThresholdError, +// }; +// use streamswap_types::stream::{Status, ThresholdState}; + +// #[test] +// fn thresholds_state() { +// let mut storage = MockStorage::new(); +// let thresholds = ThresholdState::new(); +// let mut stream = StreamState::new( +// Timestamp::from_seconds(0), +// Coin { +// denom: "out_denom".to_string(), +// amount: Uint128::from(100u128), +// }, +// "in_denom".to_string(), +// Timestamp::from_seconds(0), +// Timestamp::from_seconds(100), +// Timestamp::from_seconds(0), +// ); +// // let threshold = Uint256::from(1_500_000_000_000u128); + +// thresholds +// .set_threshold_if_any(Some(threshold), &mut storage) +// .unwrap(); + +// stream.spent_in = Uint256::from(1_500_000_000_000u128 - 1u128); +// let result = thresholds.error_if_not_reached(&storage, &stream.clone()); +// assert!(result.is_err()); +// stream.spent_in = Uint256::from(1_500_000_000_000u128); +// let result = thresholds.error_if_not_reached(&storage, &stream.clone()); +// assert!(result.is_ok()); +// } +// #[test] +// fn threshold_reached() { +// 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(1_000_000); +// let end_time = app.block_info().time.plus_seconds(5_000_000); +// let bootstrapping_start_time = app.block_info().time.plus_seconds(500_000); +// let threshold = Uint256::from(250u128); + +// 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(500, "out_denom"), +// "in_denom", +// bootstrapping_start_time, +// start_time, +// end_time, +// ) +// .url("https://sample.url".to_string()) +// .threshold(threshold) +// .build(); + +// let res = app +// .execute_contract( +// test_accounts.creator_1.clone(), +// controller_address.clone(), +// &create_stream_msg, +// &[coin(100, "fee_denom"), coin(500, "out_denom")], +// ) +// .unwrap(); +// let stream_swap_contract_address: String = get_contract_address_from_res(res); + +// let subscribe_msg = StreamSwapExecuteMsg::Subscribe {}; +// // Set time to start of the stream +// app.set_block(BlockInfo { +// time: start_time, +// height: 1_000, +// chain_id: "test".to_string(), +// }); + +// let _res = app +// .execute_contract( +// test_accounts.subscriber_1.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &subscribe_msg, +// &[coin(500, "in_denom")], +// ) +// .unwrap(); + +// // Set block to the end of the stream +// app.set_block(BlockInfo { +// time: end_time.plus_seconds(1), +// height: 1_000, +// chain_id: "test".to_string(), +// }); + +// // Threshold should be reached +// let exit_msg = StreamSwapExecuteMsg::ExitStream { salt: None }; + +// let res = app +// .execute_contract( +// test_accounts.subscriber_1.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &exit_msg, +// &[], +// ) +// .unwrap(); + +// // Exit should be possible +// // Since there is only one subscriber all out denom should be sent to subscriber +// let funds = get_funds_from_res(res); +// assert_eq!( +// Uint256::from(funds[0].1.amount.u128()), +// Uint256::from(500u128) +// ); +// assert_eq!(funds[0].1.denom, "out_denom".to_string()); + +// // Creator finalizes the stream +// let finalize_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()), +// &finalize_msg, +// &[], +// ) +// .unwrap(); + +// // Creator's revenue +// let funds = get_funds_from_res(res); +// assert_eq!( +// Uint256::from(funds[0].1.amount.u128()), +// Uint256::from(495u128) +// ); +// assert_eq!(funds[0].1.denom, "in_denom".to_string()); +// // Fee collector's revenue +// assert_eq!( +// Uint256::from(funds[1].1.amount.u128()), +// Uint256::from(5u128) +// ); +// assert_eq!(funds[1].1.denom, "in_denom".to_string()); +// } + +// #[test] +// fn threshold_not_reached() { +// 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(1_000_000); +// let end_time = app.block_info().time.plus_seconds(5_000_000); +// let bootstrapping_start_time = app.block_info().time.plus_seconds(500_000); +// let threshold = Uint256::from(500u128); + +// 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(500, "out_denom"), +// "in_denom", +// bootstrapping_start_time, +// start_time, +// end_time, +// ) +// .url("https://sample.url".to_string()) +// .threshold(threshold) +// .build(); + +// let res = app +// .execute_contract( +// test_accounts.creator_1.clone(), +// controller_address.clone(), +// &create_stream_msg, +// &[coin(100, "fee_denom"), coin(500, "out_denom")], +// ) +// .unwrap(); +// let stream_swap_contract_address: String = get_contract_address_from_res(res); + +// // Set time to start of the stream +// app.set_block(BlockInfo { +// time: start_time, +// height: 1_000, +// chain_id: "test".to_string(), +// }); + +// let subscribe_msg = StreamSwapExecuteMsg::Subscribe {}; +// // Subscription 1 +// let _res = app +// .execute_contract( +// test_accounts.subscriber_1.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &subscribe_msg, +// &[coin(250, "in_denom")], +// ) +// .unwrap(); +// // Subscription 2 +// let _res = app +// .execute_contract( +// test_accounts.subscriber_2.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &subscribe_msg, +// &[coin(1, "in_denom")], +// ) +// .unwrap(); + +// // Set block to the end of the stream +// app.set_block(BlockInfo { +// time: end_time.plus_seconds(1), +// height: 1_000, +// chain_id: "test".to_string(), +// }); + +// // Exit should not be possible +// let exit_msg = StreamSwapExecuteMsg::ExitStream { salt: None }; + +// let _res = app +// .execute_contract( +// test_accounts.subscriber_1.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &exit_msg, +// &[], +// ) +// .unwrap_err(); + +// // Finalize should not be possible +// let finalize_msg = StreamSwapExecuteMsg::FinalizeStream { +// new_treasury: None, +// create_pool: None, +// salt: None, +// }; + +// let err = app +// .execute_contract( +// test_accounts.creator_1.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &finalize_msg, +// &[], +// ) +// .unwrap_err(); +// let error = err.downcast::().unwrap(); +// assert_eq!( +// error, +// StreamSwapError::ThresholdError(ThresholdError::ThresholdNotReached {}) +// ); + +// // Subscriber one executes exit cancelled before creator cancels stream +// let exit_cancelled_msg = StreamSwapExecuteMsg::ExitCancelled {}; +// // Subscriber 1 executes exit cancelled +// let res = app +// .execute_contract( +// test_accounts.subscriber_1.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &exit_cancelled_msg, +// &[], +// ) +// .unwrap(); +// let subscriber_1_funds = get_funds_from_res(res); +// assert_eq!(subscriber_1_funds.len(), 1); +// assert_eq!( +// Uint256::from(subscriber_1_funds[0].1.amount.u128()), +// Uint256::from(250u128) +// ); +// assert_eq!(subscriber_1_funds[0].1.denom, "in_denom".to_string()); + +// // Creator threshold cancels the stream +// let cancel_msg = StreamSwapExecuteMsg::CancelStreamWithThreshold {}; + +// let res = app +// .execute_contract( +// test_accounts.creator_1.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &cancel_msg, +// &[], +// ) +// .unwrap(); +// let creator_funds = get_funds_from_res(res); +// assert_eq!(creator_funds.len(), 1); +// assert_eq!( +// Uint256::from(creator_funds[0].1.amount.u128()), +// Uint256::from(500u128) +// ); +// assert_eq!(creator_funds[0].1.denom, "out_denom".to_string()); + +// // Creator can not finalize the stream +// let finalize_msg = StreamSwapExecuteMsg::FinalizeStream { +// new_treasury: None, +// create_pool: None, +// salt: None, +// }; + +// let err = app +// .execute_contract( +// test_accounts.creator_1.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &finalize_msg, +// &[], +// ) +// .unwrap_err(); +// let error = err.downcast::().unwrap(); +// assert_eq!( +// error, +// StreamSwapError::OperationNotAllowed { +// current_status: "Cancelled".to_string() +// } +// ); + +// // Creator can not cancel the stream again +// let cancel_msg = StreamSwapExecuteMsg::CancelStreamWithThreshold {}; + +// let err = app +// .execute_contract( +// test_accounts.creator_1.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &cancel_msg, +// &[], +// ) +// .unwrap_err(); +// let error = err.downcast::().unwrap(); +// assert_eq!( +// error, +// StreamSwapError::OperationNotAllowed { +// current_status: "Cancelled".to_string() +// } +// ); + +// // Subscriber 2 executes exit cancelled after creator cancels stream +// let exit_cancelled_msg = StreamSwapExecuteMsg::ExitCancelled {}; + +// let res = app +// .execute_contract( +// test_accounts.subscriber_2.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &exit_cancelled_msg, +// &[], +// ) +// .unwrap(); +// let subscriber_2_funds = get_funds_from_res(res); +// assert_eq!(subscriber_2_funds.len(), 1); +// assert_eq!( +// Uint256::from(subscriber_2_funds[0].1.amount.u128()), +// Uint256::from(1u128) +// ); +// assert_eq!(subscriber_2_funds[0].1.denom, "in_denom".to_string()); + +// // Query stream should return stream with is_cancelled = true +// let query_msg = StreamSwapQueryMsg::Stream {}; + +// let res: StreamResponse = app +// .wrap() +// .query_wasm_smart( +// Addr::unchecked(stream_swap_contract_address.clone()), +// &query_msg, +// ) +// .unwrap(); + +// assert_eq!(res.status, Status::Cancelled); +// } + +// #[test] +// fn threshold_cancel() { +// 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(1_000_000); +// let end_time = app.block_info().time.plus_seconds(5_000_000); +// let bootstrapping_start_time = app.block_info().time.plus_seconds(500_000); +// let threshold = Uint256::from(500u128); + +// 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(500, "out_denom"), +// "in_denom", +// bootstrapping_start_time, +// start_time, +// end_time, +// ) +// .url("https://sample.url".to_string()) +// .threshold(threshold) +// .build(); + +// let res = app +// .execute_contract( +// test_accounts.creator_1.clone(), +// controller_address.clone(), +// &create_stream_msg, +// &[coin(100, "fee_denom"), coin(500, "out_denom")], +// ) +// .unwrap(); +// let stream_swap_contract_address: String = get_contract_address_from_res(res); +// // Set time to start of the stream +// app.set_block(BlockInfo { +// time: start_time, +// height: 1_000, +// chain_id: "test".to_string(), +// }); + +// // Subscription 1 +// let subscribe_msg = StreamSwapExecuteMsg::Subscribe {}; + +// let _res = app +// .execute_contract( +// test_accounts.subscriber_1.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &subscribe_msg, +// &[coin(250, "in_denom")], +// ) +// .unwrap(); + +// // Subscription 2 +// let _res = app +// .execute_contract( +// test_accounts.subscriber_2.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &subscribe_msg, +// &[coin(250 - 1, "in_denom")], +// ) +// .unwrap(); + +// // Can not cancel stream before it ends +// let cancel_msg = StreamSwapExecuteMsg::CancelStreamWithThreshold {}; + +// let err = app +// .execute_contract( +// test_accounts.creator_1.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &cancel_msg, +// &[], +// ) +// .unwrap_err(); +// let error = err.downcast::().unwrap(); +// assert_eq!( +// error, +// StreamSwapError::OperationNotAllowed { +// current_status: "Active".to_string() +// } +// ); + +// // Set block to the end of the stream +// app.set_block(BlockInfo { +// time: end_time.plus_seconds(1), +// height: 1_000, +// chain_id: "test".to_string(), +// }); + +// // Non creator can't cancel stream +// let cancel_msg = StreamSwapExecuteMsg::CancelStreamWithThreshold {}; + +// let err = app +// .execute_contract( +// test_accounts.subscriber_1.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &cancel_msg, +// &[], +// ) +// .unwrap_err(); + +// let error = err.downcast::().unwrap(); + +// assert_eq!(error, StreamSwapError::Unauthorized {}); + +// // Creator can cancel stream +// let cancel_msg = StreamSwapExecuteMsg::CancelStreamWithThreshold {}; + +// let _res = app +// .execute_contract( +// test_accounts.creator_1.clone(), +// Addr::unchecked(stream_swap_contract_address.clone()), +// &cancel_msg, +// &[], +// ) +// .unwrap(); + +// // Query stream should return stream with is_cancelled = true + +// let query_msg = StreamSwapQueryMsg::Stream {}; + +// let res: StreamResponse = app +// .wrap() +// .query_wasm_smart( +// Addr::unchecked(stream_swap_contract_address.clone()), +// &query_msg, +// ) +// .unwrap(); + +// assert_eq!(res.status, Status::Cancelled); +// } +// } diff --git a/tests/src/tests/streamswap_tests/vesting.rs b/tests/src/tests/streamswap_tests/vesting.rs index b64ac051..feb149fa 100644 --- a/tests/src/tests/streamswap_tests/vesting.rs +++ b/tests/src/tests/streamswap_tests/vesting.rs @@ -11,7 +11,8 @@ mod vesting { use cw_vesting::CheckedDenom; use streamswap_types::controller::VestingConfig; use streamswap_types::stream::{ - ExecuteMsg as StreamSwapExecuteMsg, QueryMsg as StreamSwapQueryMsg, Status, StreamResponse, + ExecuteMsg as StreamSwapExecuteMsg, FinalizedStatus, QueryMsg as StreamSwapQueryMsg, + Status, StreamResponse, }; #[test] @@ -121,7 +122,10 @@ mod vesting { ) .unwrap(); - assert_eq!(stream.status, Status::Finalized); + assert_eq!( + stream.status, + Status::Finalized(FinalizedStatus::ThresholdReached) + ); // no salt expect error let exit_msg = StreamSwapExecuteMsg::ExitStream { salt: None }; @@ -277,7 +281,10 @@ mod vesting { ) .unwrap(); - assert_eq!(stream.status, Status::Finalized); + assert_eq!( + stream.status, + Status::Finalized(FinalizedStatus::ThresholdReached) + ); let vesting_addr = get_wasm_attribute_with_key(res.clone(), "vesting_address".to_string()); let contract_data = app diff --git a/ts/types/StreamSwapFactory.client.ts b/ts/types/StreamSwapFactory.client.ts deleted file mode 100644 index 3f33feea..00000000 --- a/ts/types/StreamSwapFactory.client.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** -* This file was automatically generated by @cosmwasm/ts-codegen@0.35.7. -* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, -* and run the @cosmwasm/ts-codegen generate command to regenerate this file. -*/ - -import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate"; -import { StdFee } from "@cosmjs/amino"; -import { Decimal, Uint128, InstantiateMsg, Coin, ExecuteMsg, Timestamp, Uint64, UncheckedDenom, Schedule, CreateStreamMsg, CreatePool, MsgCreateConcentratedPool, QueryMsg, Boolean, Addr, Params } from "./StreamSwapController.types"; -export interface StreamSwapControllerReadOnlyInterface { - contractAddress: string; - params: () => Promise; - freezestate: () => Promise; - lastStreamId: () => Promise; -} -export class StreamSwapControllerQueryClient implements StreamSwapControllerReadOnlyInterface { - client: CosmWasmClient; - contractAddress: string; - - constructor(client: CosmWasmClient, contractAddress: string) { - this.client = client; - this.contractAddress = contractAddress; - this.params = this.params.bind(this); - this.freezestate = this.freezestate.bind(this); - this.lastStreamId = this.lastStreamId.bind(this); - } - - params = async (): Promise => { - return this.client.queryContractSmart(this.contractAddress, { - params: {} - }); - }; - freezestate = async (): Promise => { - return this.client.queryContractSmart(this.contractAddress, { - freezestate: {} - }); - }; - lastStreamId = async (): Promise => { - return this.client.queryContractSmart(this.contractAddress, { - last_stream_id: {} - }); - }; -} -export interface StreamSwapControllerInterface extends StreamSwapControllerReadOnlyInterface { - contractAddress: string; - sender: string; - updateParams: ({ - acceptedInDenoms, - exitFeePercent, - feeCollector, - minSecondsUntilStartTime, - minStreamSeconds, - streamCreationFee - }: { - acceptedInDenoms?: string[]; - exitFeePercent?: Decimal; - feeCollector?: string; - minSecondsUntilStartTime?: number; - minStreamSeconds?: number; - streamCreationFee?: Coin; - }, fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; - createStream: ({ - msg - }: { - msg: CreateStreamMsg; - }, fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; - freeze: (fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; -} -export class StreamSwapControllerClient extends StreamSwapControllerQueryClient implements StreamSwapControllerInterface { - client: SigningCosmWasmClient; - sender: string; - contractAddress: string; - - constructor(client: SigningCosmWasmClient, sender: string, contractAddress: string) { - super(client, contractAddress); - this.client = client; - this.sender = sender; - this.contractAddress = contractAddress; - this.updateParams = this.updateParams.bind(this); - this.createStream = this.createStream.bind(this); - this.freeze = this.freeze.bind(this); - } - - updateParams = async ({ - acceptedInDenoms, - exitFeePercent, - feeCollector, - minSecondsUntilStartTime, - minStreamSeconds, - streamCreationFee - }: { - acceptedInDenoms?: string[]; - exitFeePercent?: Decimal; - feeCollector?: string; - minSecondsUntilStartTime?: number; - minStreamSeconds?: number; - streamCreationFee?: Coin; - }, fee: number | StdFee | "auto" = "auto", memo?: string, _funds?: Coin[]): Promise => { - return await this.client.execute(this.sender, this.contractAddress, { - update_params: { - accepted_in_denoms: acceptedInDenoms, - exit_fee_percent: exitFeePercent, - fee_collector: feeCollector, - min_seconds_until_start_time: minSecondsUntilStartTime, - min_stream_seconds: minStreamSeconds, - stream_creation_fee: streamCreationFee - } - }, fee, memo, _funds); - }; - createStream = async ({ - msg - }: { - msg: CreateStreamMsg; - }, fee: number | StdFee | "auto" = "auto", memo?: string, _funds?: Coin[]): Promise => { - return await this.client.execute(this.sender, this.contractAddress, { - create_stream: { - msg - } - }, fee, memo, _funds); - }; - freeze = async (fee: number | StdFee | "auto" = "auto", memo?: string, _funds?: Coin[]): Promise => { - return await this.client.execute(this.sender, this.contractAddress, { - freeze: {} - }, fee, memo, _funds); - }; -} \ No newline at end of file diff --git a/ts/types/StreamSwapFactory.message-composer.ts b/ts/types/StreamSwapFactory.message-composer.ts deleted file mode 100644 index 777eb300..00000000 --- a/ts/types/StreamSwapFactory.message-composer.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** -* This file was automatically generated by @cosmwasm/ts-codegen@0.35.7. -* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, -* and run the @cosmwasm/ts-codegen generate command to regenerate this file. -*/ - -import { MsgExecuteContractEncodeObject } from "@cosmjs/cosmwasm-stargate"; -import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; -import { toUtf8 } from "@cosmjs/encoding"; -import { Decimal, Uint128, InstantiateMsg, Coin, ExecuteMsg, Timestamp, Uint64, UncheckedDenom, Schedule, CreateStreamMsg, CreatePool, MsgCreateConcentratedPool, QueryMsg, Boolean, Addr, Params } from "./StreamSwapController.types"; -export interface StreamSwapControllerMsg { - contractAddress: string; - sender: string; - updateParams: ({ - acceptedInDenoms, - exitFeePercent, - feeCollector, - minSecondsUntilStartTime, - minStreamSeconds, - streamCreationFee - }: { - acceptedInDenoms?: string[]; - exitFeePercent?: Decimal; - feeCollector?: string; - minSecondsUntilStartTime?: number; - minStreamSeconds?: number; - streamCreationFee?: Coin; - }, _funds?: Coin[]) => MsgExecuteContractEncodeObject; - createStream: ({ - msg - }: { - msg: CreateStreamMsg; - }, _funds?: Coin[]) => MsgExecuteContractEncodeObject; - freeze: (_funds?: Coin[]) => MsgExecuteContractEncodeObject; -} -export class StreamSwapControllerMsgComposer implements StreamSwapControllerMsg { - sender: string; - contractAddress: string; - - constructor(sender: string, contractAddress: string) { - this.sender = sender; - this.contractAddress = contractAddress; - this.updateParams = this.updateParams.bind(this); - this.createStream = this.createStream.bind(this); - this.freeze = this.freeze.bind(this); - } - - updateParams = ({ - acceptedInDenoms, - exitFeePercent, - feeCollector, - minSecondsUntilStartTime, - minStreamSeconds, - streamCreationFee - }: { - acceptedInDenoms?: string[]; - exitFeePercent?: Decimal; - feeCollector?: string; - minSecondsUntilStartTime?: number; - minStreamSeconds?: number; - streamCreationFee?: Coin; - }, _funds?: Coin[]): MsgExecuteContractEncodeObject => { - return { - typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", - value: MsgExecuteContract.fromPartial({ - sender: this.sender, - contract: this.contractAddress, - msg: toUtf8(JSON.stringify({ - update_params: { - accepted_in_denoms: acceptedInDenoms, - exit_fee_percent: exitFeePercent, - fee_collector: feeCollector, - min_seconds_until_start_time: minSecondsUntilStartTime, - min_stream_seconds: minStreamSeconds, - stream_creation_fee: streamCreationFee - } - })), - funds: _funds - }) - }; - }; - createStream = ({ - msg - }: { - msg: CreateStreamMsg; - }, _funds?: Coin[]): MsgExecuteContractEncodeObject => { - return { - typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", - value: MsgExecuteContract.fromPartial({ - sender: this.sender, - contract: this.contractAddress, - msg: toUtf8(JSON.stringify({ - create_stream: { - msg - } - })), - funds: _funds - }) - }; - }; - freeze = (_funds?: Coin[]): MsgExecuteContractEncodeObject => { - return { - typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", - value: MsgExecuteContract.fromPartial({ - sender: this.sender, - contract: this.contractAddress, - msg: toUtf8(JSON.stringify({ - freeze: {} - })), - funds: _funds - }) - }; - }; -} \ No newline at end of file diff --git a/ts/types/StreamSwapFactory.types.ts b/ts/types/StreamSwapFactory.types.ts deleted file mode 100644 index 9e3111ea..00000000 --- a/ts/types/StreamSwapFactory.types.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** -* This file was automatically generated by @cosmwasm/ts-codegen@0.35.7. -* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, -* and run the @cosmwasm/ts-codegen generate command to regenerate this file. -*/ - -export type Decimal = string; -export type Uint128 = string; -export interface InstantiateMsg { - denom: UncheckedDenom; - description?: string | null; - owner?: string | null; - recipient: string; - schedule: Schedule; - start_time?: Timestamp | null; - title: string; - total: Uint128; - unbonding_duration_seconds: number; - vesting_duration_seconds: number; -} -export interface Coin { - amount: Uint128; - denom: string; - [k: string]: unknown; -} -export type ExecuteMsg = { - update_params: { - accepted_in_denoms?: string[] | null; - exit_fee_percent?: Decimal | null; - fee_collector?: string | null; - min_seconds_until_start_time?: number | null; - min_stream_seconds?: number | null; - stream_creation_fee?: Coin | null; - }; -} | { - create_stream: { - msg: CreateStreamMsg; - }; -} | { - freeze: {}; -}; -export type Timestamp = Uint64; -export type Uint64 = number; -export type UncheckedDenom = { - native: string; -} | { - cw20: string; -}; -export type Schedule = "saturating_linear" | { - piecewise_linear: [number, Uint128][]; -}; -export interface CreateStreamMsg { - create_pool?: CreatePool | null; - end_time: Timestamp; - in_denom: string; - name: string; - out_asset: Coin; - start_time: Timestamp; - stream_admin: string; - threshold?: Uint128 | null; - treasury: string; - url?: string | null; - vesting?: InstantiateMsg | null; -} -export interface CreatePool { - msg_create_pool: MsgCreateConcentratedPool; - out_amount_clp: Uint128; -} -export interface MsgCreateConcentratedPool { - denom0: string; - denom1: string; - sender: string; - spread_factor: string; - tick_spacing: number; - [k: string]: unknown; -} -export type QueryMsg = { - params: {}; -} | { - freezestate: {}; -} | { - last_stream_id: {}; -}; -export type Boolean = boolean; -export type Addr = string; -export interface Params { - accepted_in_denoms: string[]; - exit_fee_percent: Decimal; - fee_collector: Addr; - min_seconds_until_start_time: number; - min_stream_seconds: number; - protocol_admin: Addr; - stream_creation_fee: Coin; - stream_swap_code_id: number; - vesting_code_id: number; -} \ No newline at end of file diff --git a/ts/types/StreamSwapStream.client.ts b/ts/types/StreamSwapStream.client.ts index 7a2d2adf..b83ec742 100644 --- a/ts/types/StreamSwapStream.client.ts +++ b/ts/types/StreamSwapStream.client.ts @@ -6,7 +6,7 @@ import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate"; import { StdFee } from "@cosmjs/amino"; -import { Timestamp, Uint64, Schedule, Uint128, PoolConfig, Uint256, Binary, InstantiateMsg, VestingConfig, Coin, ExecuteMsg, CreatePool, QueryMsg, Decimal256, AveragePriceResponse, LatestStreamedPriceResponse, PositionsResponse, PositionResponse, Addr, Params, Status, StreamResponse, String } from "./StreamSwapStream.types"; +import { Timestamp, Uint64, Schedule, Uint128, PoolConfig, Uint256, Binary, InstantiateMsg, VestingConfig, Coin, ExecuteMsg, CreatePool, QueryMsg, Decimal256, AveragePriceResponse, String, LatestStreamedPriceResponse, PositionsResponse, PositionResponse, Addr, Params, Status, FinalizedStatus, StreamResponse } from "./StreamSwapStream.types"; export interface StreamSwapStreamReadOnlyInterface { contractAddress: string; params: () => Promise; @@ -25,12 +25,17 @@ export interface StreamSwapStreamReadOnlyInterface { }) => Promise; averagePrice: () => Promise; lastStreamedPrice: () => Promise; - threshold: () => Promise; toS: ({ addr }: { addr?: string; }) => Promise; + creatorVesting: () => Promise; + subscriberVesting: ({ + addr + }: { + addr: string; + }) => Promise; } export class StreamSwapStreamQueryClient implements StreamSwapStreamReadOnlyInterface { client: CosmWasmClient; @@ -45,8 +50,9 @@ export class StreamSwapStreamQueryClient implements StreamSwapStreamReadOnlyInte this.listPositions = this.listPositions.bind(this); this.averagePrice = this.averagePrice.bind(this); this.lastStreamedPrice = this.lastStreamedPrice.bind(this); - this.threshold = this.threshold.bind(this); this.toS = this.toS.bind(this); + this.creatorVesting = this.creatorVesting.bind(this); + this.subscriberVesting = this.subscriberVesting.bind(this); } params = async (): Promise => { @@ -94,11 +100,6 @@ export class StreamSwapStreamQueryClient implements StreamSwapStreamReadOnlyInte last_streamed_price: {} }); }; - threshold = async (): Promise => { - return this.client.queryContractSmart(this.contractAddress, { - threshold: {} - }); - }; toS = async ({ addr }: { @@ -110,6 +111,22 @@ export class StreamSwapStreamQueryClient implements StreamSwapStreamReadOnlyInte } }); }; + creatorVesting = async (): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + creator_vesting: {} + }); + }; + subscriberVesting = async ({ + addr + }: { + addr: string; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + subscriber_vesting: { + addr + } + }); + }; } export interface StreamSwapStreamInterface extends StreamSwapStreamReadOnlyInterface { contractAddress: string; @@ -136,9 +153,7 @@ export interface StreamSwapStreamInterface extends StreamSwapStreamReadOnlyInter }: { salt?: Binary; }, fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; - exitCancelled: (fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; cancelStream: (fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; - cancelStreamWithThreshold: (fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; streamAdminCancel: (fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; } export class StreamSwapStreamClient extends StreamSwapStreamQueryClient implements StreamSwapStreamInterface { @@ -157,9 +172,7 @@ export class StreamSwapStreamClient extends StreamSwapStreamQueryClient implemen this.syncPosition = this.syncPosition.bind(this); this.finalizeStream = this.finalizeStream.bind(this); this.exitStream = this.exitStream.bind(this); - this.exitCancelled = this.exitCancelled.bind(this); this.cancelStream = this.cancelStream.bind(this); - this.cancelStreamWithThreshold = this.cancelStreamWithThreshold.bind(this); this.streamAdminCancel = this.streamAdminCancel.bind(this); } @@ -217,21 +230,11 @@ export class StreamSwapStreamClient extends StreamSwapStreamQueryClient implemen } }, fee, memo, _funds); }; - exitCancelled = async (fee: number | StdFee | "auto" = "auto", memo?: string, _funds?: Coin[]): Promise => { - return await this.client.execute(this.sender, this.contractAddress, { - exit_cancelled: {} - }, fee, memo, _funds); - }; cancelStream = async (fee: number | StdFee | "auto" = "auto", memo?: string, _funds?: Coin[]): Promise => { return await this.client.execute(this.sender, this.contractAddress, { cancel_stream: {} }, fee, memo, _funds); }; - cancelStreamWithThreshold = async (fee: number | StdFee | "auto" = "auto", memo?: string, _funds?: Coin[]): Promise => { - return await this.client.execute(this.sender, this.contractAddress, { - cancel_stream_with_threshold: {} - }, fee, memo, _funds); - }; streamAdminCancel = async (fee: number | StdFee | "auto" = "auto", memo?: string, _funds?: Coin[]): Promise => { return await this.client.execute(this.sender, this.contractAddress, { stream_admin_cancel: {} diff --git a/ts/types/StreamSwapStream.message-composer.ts b/ts/types/StreamSwapStream.message-composer.ts index 3e635c8f..f4f684c6 100644 --- a/ts/types/StreamSwapStream.message-composer.ts +++ b/ts/types/StreamSwapStream.message-composer.ts @@ -7,7 +7,7 @@ import { MsgExecuteContractEncodeObject } from "@cosmjs/cosmwasm-stargate"; import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; import { toUtf8 } from "@cosmjs/encoding"; -import { Timestamp, Uint64, Schedule, Uint128, PoolConfig, Uint256, Binary, InstantiateMsg, VestingConfig, Coin, ExecuteMsg, CreatePool, QueryMsg, Decimal256, AveragePriceResponse, LatestStreamedPriceResponse, PositionsResponse, PositionResponse, Addr, Params, Status, StreamResponse, String } from "./StreamSwapStream.types"; +import { Timestamp, Uint64, Schedule, Uint128, PoolConfig, Uint256, Binary, InstantiateMsg, VestingConfig, Coin, ExecuteMsg, CreatePool, QueryMsg, Decimal256, AveragePriceResponse, String, LatestStreamedPriceResponse, PositionsResponse, PositionResponse, Addr, Params, Status, FinalizedStatus, StreamResponse } from "./StreamSwapStream.types"; export interface StreamSwapStreamMsg { contractAddress: string; sender: string; @@ -33,9 +33,7 @@ export interface StreamSwapStreamMsg { }: { salt?: Binary; }, _funds?: Coin[]) => MsgExecuteContractEncodeObject; - exitCancelled: (_funds?: Coin[]) => MsgExecuteContractEncodeObject; cancelStream: (_funds?: Coin[]) => MsgExecuteContractEncodeObject; - cancelStreamWithThreshold: (_funds?: Coin[]) => MsgExecuteContractEncodeObject; streamAdminCancel: (_funds?: Coin[]) => MsgExecuteContractEncodeObject; } export class StreamSwapStreamMsgComposer implements StreamSwapStreamMsg { @@ -51,9 +49,7 @@ export class StreamSwapStreamMsgComposer implements StreamSwapStreamMsg { this.syncPosition = this.syncPosition.bind(this); this.finalizeStream = this.finalizeStream.bind(this); this.exitStream = this.exitStream.bind(this); - this.exitCancelled = this.exitCancelled.bind(this); this.cancelStream = this.cancelStream.bind(this); - this.cancelStreamWithThreshold = this.cancelStreamWithThreshold.bind(this); this.streamAdminCancel = this.streamAdminCancel.bind(this); } @@ -159,19 +155,6 @@ export class StreamSwapStreamMsgComposer implements StreamSwapStreamMsg { }) }; }; - exitCancelled = (_funds?: Coin[]): MsgExecuteContractEncodeObject => { - return { - typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", - value: MsgExecuteContract.fromPartial({ - sender: this.sender, - contract: this.contractAddress, - msg: toUtf8(JSON.stringify({ - exit_cancelled: {} - })), - funds: _funds - }) - }; - }; cancelStream = (_funds?: Coin[]): MsgExecuteContractEncodeObject => { return { typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", @@ -185,19 +168,6 @@ export class StreamSwapStreamMsgComposer implements StreamSwapStreamMsg { }) }; }; - cancelStreamWithThreshold = (_funds?: Coin[]): MsgExecuteContractEncodeObject => { - return { - typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", - value: MsgExecuteContract.fromPartial({ - sender: this.sender, - contract: this.contractAddress, - msg: toUtf8(JSON.stringify({ - cancel_stream_with_threshold: {} - })), - funds: _funds - }) - }; - }; streamAdminCancel = (_funds?: Coin[]): MsgExecuteContractEncodeObject => { return { typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", diff --git a/ts/types/StreamSwapStream.types.ts b/ts/types/StreamSwapStream.types.ts index 6417bfdd..de25d1cc 100644 --- a/ts/types/StreamSwapStream.types.ts +++ b/ts/types/StreamSwapStream.types.ts @@ -64,12 +64,8 @@ export type ExecuteMsg = { exit_stream: { salt?: Binary | null; }; -} | { - exit_cancelled: {}; } | { cancel_stream: {}; -} | { - cancel_stream_with_threshold: {}; } | { stream_admin_cancel: {}; }; @@ -98,17 +94,22 @@ export type QueryMsg = { average_price: {}; } | { last_streamed_price: {}; -} | { - threshold: {}; } | { to_s: { addr?: string | null; }; +} | { + creator_vesting: {}; +} | { + subscriber_vesting: { + addr: string; + }; }; export type Decimal256 = string; export interface AveragePriceResponse { average_price: Decimal256; } +export type String = string; export interface LatestStreamedPriceResponse { current_streamed_price: Decimal256; } @@ -140,7 +141,10 @@ export interface Params { tos_version: string; vesting_code_id: number; } -export type Status = "waiting" | "bootstrapping" | "active" | "ended" | "finalized" | "cancelled"; +export type Status = "waiting" | "bootstrapping" | "active" | "ended" | { + finalized: FinalizedStatus; +} | "cancelled"; +export type FinalizedStatus = "threshold_reached" | "threshold_not_reached"; export interface StreamResponse { current_streamed_price: Decimal256; dist_index: Decimal256; @@ -157,5 +161,4 @@ export interface StreamResponse { stream_admin: string; treasury: string; url?: string | null; -} -export type String = string; \ No newline at end of file +} \ No newline at end of file