From b16dc4805081c90c79d87a7f42771c16f9edbbee Mon Sep 17 00:00:00 2001 From: cowboy0015 Date: Mon, 29 Apr 2024 18:16:41 -0400 Subject: [PATCH 01/11] fix: rebuilt existing crowdfund app with basic instantiate functionality --- .../andromeda-crowdfund/src/contract.rs | 1618 ++++--- .../andromeda-crowdfund/src/mock.rs | 292 +- .../andromeda-crowdfund/src/state.rs | 55 +- .../src/testing/mock_querier.rs | 251 +- .../andromeda-crowdfund/src/testing/tests.rs | 3876 ++++++++--------- .../src/crowdfund.rs | 155 +- packages/std/src/error.rs | 3 + tests-integration/tests/crowdfund_app.rs | 249 -- tests-integration/tests/mod.rs | 3 - 9 files changed, 2923 insertions(+), 3579 deletions(-) delete mode 100644 tests-integration/tests/crowdfund_app.rs diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs index 26c780f02..976ea1d0a 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs @@ -1,45 +1,25 @@ -use crate::state::{ - get_available_tokens, Purchase, AVAILABLE_TOKENS, CONFIG, NUMBER_OF_TOKENS_AVAILABLE, - PURCHASES, SALE_CONDUCTED, STATE, -}; -use andromeda_non_fungible_tokens::{ - crowdfund::{ - Config, CrowdfundMintMsg, ExecuteMsg, InstantiateMsg, IsTokenAvailableResponse, QueryMsg, - State, - }, - cw721::{ExecuteMsg as Cw721ExecuteMsg, MintMsg, QueryMsg as Cw721QueryMsg}, -}; -use andromeda_std::{ - ado_base::ownership::OwnershipMessage, - amp::{messages::AMPPkt, recipient::Recipient, AndrAddr}, - common::{ - actions::call_action, - expiration::{expiration_from_milliseconds, get_and_validate_start_time, Expiry}, - }, +// use crate::state::{ +// get_available_tokens, Purchase, AVAILABLE_TOKENS, CONFIG, NUMBER_OF_TOKENS_AVAILABLE, +// PURCHASES, SALE_CONDUCTED, STATE, +// }; +use andromeda_non_fungible_tokens::crowdfund::{ + CampaignConfig, ExecuteMsg, InstantiateMsg, QueryMsg, }; +use andromeda_std::{ado_base::ownership::OwnershipMessage, common::actions::call_action}; use andromeda_std::{ado_contract::ADOContract, common::context::ExecuteContext}; -use andromeda_std::common::denom::validate_denom; use andromeda_std::{ ado_base::{hooks::AndromedaHook, InstantiateMsg as BaseInstantiateMsg, MigrateMsg}, - common::{deduct_funds, encode_binary, merge_sub_msgs, rates::get_tax_amount, Funds}, + common::encode_binary, error::ContractError, }; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{ - coins, ensure, has_coins, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, - Order, QuerierWrapper, QueryRequest, Reply, Response, StdError, Storage, SubMsg, Uint128, - WasmMsg, WasmQuery, -}; -use cw721::{ContractInfoResponse, TokensResponse}; -use cw_utils::nonpayable; -use std::cmp; +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdError}; + +use crate::state::CAMPAIGN_CONFIG; -const MAX_LIMIT: u32 = 100; -const DEFAULT_LIMIT: u32 = 50; -pub(crate) const MAX_MINT_LIMIT: u32 = 100; const CONTRACT_NAME: &str = "crates.io:andromeda-crowdfund"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -50,15 +30,9 @@ pub fn instantiate( info: MessageInfo, msg: InstantiateMsg, ) -> Result { - CONFIG.save( - deps.storage, - &Config { - token_address: msg.token_address, - can_mint_after_sale: msg.can_mint_after_sale, - }, - )?; - SALE_CONDUCTED.save(deps.storage, &false)?; - NUMBER_OF_TOKENS_AVAILABLE.save(deps.storage, &Uint128::zero())?; + let config: CampaignConfig = msg.campaign_config; + + CAMPAIGN_CONFIG.save(deps.storage, &config)?; let inst_resp = ADOContract::default().instantiate( deps.storage, env, @@ -92,6 +66,11 @@ pub fn reply(_deps: DepsMut, _env: Env, msg: Reply) -> Result Result { + ADOContract::default().migrate(deps, CONTRACT_NAME, CONTRACT_VERSION) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( deps: DepsMut, @@ -132,794 +111,793 @@ pub fn handle_execute(mut ctx: ExecuteContext, msg: ExecuteMsg) -> Result execute_mint(ctx, mint_msgs), - ExecuteMsg::StartSale { - start_time, - end_time, - price, - min_tokens_sold, - max_amount_per_wallet, - recipient, - } => execute_start_sale( - ctx, - start_time, - end_time, - price, - min_tokens_sold, - max_amount_per_wallet, - recipient, - ), - ExecuteMsg::Purchase { number_of_tokens } => execute_purchase(ctx, number_of_tokens), - ExecuteMsg::PurchaseByTokenId { token_id } => execute_purchase_by_token_id(ctx, token_id), - ExecuteMsg::ClaimRefund {} => execute_claim_refund(ctx), - ExecuteMsg::EndSale { limit } => execute_end_sale(ctx, limit), - ExecuteMsg::UpdateTokenContract { address } => execute_update_token_contract(ctx, address), - _ => ADOContract::default().execute(ctx, msg), - }?; + + let res = ADOContract::default().execute(ctx, msg)?; Ok(res .add_submessages(action_response.messages) .add_attributes(action_response.attributes) .add_events(action_response.events)) -} - -fn execute_mint( - ctx: ExecuteContext, - mint_msgs: Vec, -) -> Result { - let ExecuteContext { - deps, info, env, .. - } = ctx; - nonpayable(&info)?; - - ensure!( - mint_msgs.len() <= MAX_MINT_LIMIT as usize, - ContractError::TooManyMintMessages { - limit: MAX_MINT_LIMIT, - } - ); - let contract = ADOContract::default(); - ensure!( - contract.is_contract_owner(deps.storage, info.sender.as_str())?, - ContractError::Unauthorized {} - ); - // Can only mint when no sale is ongoing. - ensure!( - STATE.may_load(deps.storage)?.is_none(), - ContractError::SaleStarted {} - ); - let sale_conducted = SALE_CONDUCTED.load(deps.storage)?; - let config = CONFIG.load(deps.storage)?; - ensure!( - config.can_mint_after_sale || !sale_conducted, - ContractError::CannotMintAfterSaleConducted {} - ); - - let token_contract = config.token_address; - let crowdfund_contract = env.contract.address.to_string(); - let resolved_path = token_contract.get_raw_address(&deps.as_ref())?; - - let mut resp = Response::new(); - for mint_msg in mint_msgs { - let mint_resp = mint( - deps.storage, - &crowdfund_contract, - resolved_path.to_string(), - mint_msg, - )?; - resp = resp - .add_attributes(mint_resp.attributes) - .add_submessages(mint_resp.messages); - } - - Ok(resp) -} - -fn mint( - storage: &mut dyn Storage, - crowdfund_contract: &str, - token_contract: String, - mint_msg: CrowdfundMintMsg, -) -> Result { - let mint_msg: MintMsg = MintMsg { - token_id: mint_msg.token_id, - owner: mint_msg - .owner - .unwrap_or_else(|| crowdfund_contract.to_owned()), - token_uri: mint_msg.token_uri, - extension: mint_msg.extension, - }; - // We allow for owners other than the contract, incase the creator wants to set aside a few - // tokens for some other use, say airdrop, team allocation, etc. Only those which have the - // contract as the owner will be available to sell. - if mint_msg.owner == crowdfund_contract { - // Mark token as available to purchase in next sale. - AVAILABLE_TOKENS.save(storage, &mint_msg.token_id, &true)?; - let current_number = NUMBER_OF_TOKENS_AVAILABLE.load(storage)?; - NUMBER_OF_TOKENS_AVAILABLE.save(storage, &(current_number + Uint128::new(1)))?; - } - Ok(Response::new() - .add_attribute("action", "mint") - .add_message(WasmMsg::Execute { - contract_addr: token_contract, - msg: encode_binary(&Cw721ExecuteMsg::Mint { - token_id: mint_msg.token_id, - owner: mint_msg.owner, - token_uri: mint_msg.token_uri, - extension: mint_msg.extension, - })?, - funds: vec![], - })) -} - -fn execute_update_token_contract( - ctx: ExecuteContext, - address: AndrAddr, -) -> Result { - let ExecuteContext { deps, info, .. } = ctx; - nonpayable(&info)?; - - let contract = ADOContract::default(); - ensure!( - contract.is_contract_owner(deps.storage, info.sender.as_str())?, - ContractError::Unauthorized {} - ); - // Ensure no tokens have been minted already - let num_tokens = NUMBER_OF_TOKENS_AVAILABLE - .load(deps.storage) - .unwrap_or(Uint128::zero()); - ensure!(num_tokens.is_zero(), ContractError::Unauthorized {}); - - // Will error if not a valid path - let addr = address.get_raw_address(&deps.as_ref())?; - let query = Cw721QueryMsg::ContractInfo {}; - - // Check contract is a valid CW721 contract - let res: Result = deps.querier.query_wasm_smart(addr, &query); - ensure!(res.is_ok(), ContractError::Unauthorized {}); - - CONFIG.update(deps.storage, |mut config| { - config.token_address = address; - Ok::<_, ContractError>(config) - })?; - Ok(Response::new().add_attribute("action", "update_token_contract")) -} - -#[allow(clippy::too_many_arguments)] -fn execute_start_sale( - ctx: ExecuteContext, - start_time: Option, - end_time: Expiry, - price: Coin, - min_tokens_sold: Uint128, - max_amount_per_wallet: Option, - recipient: Recipient, -) -> Result { - let ExecuteContext { - deps, info, env, .. - } = ctx; - validate_denom(deps.as_ref(), price.denom.clone())?; - recipient.validate(&deps.as_ref())?; - nonpayable(&info)?; - let ado_contract = ADOContract::default(); - - // Validate recipient - ado_contract.validate_andr_addresses(&deps.as_ref(), vec![recipient.address.clone()])?; - ensure!( - ADOContract::default().is_contract_owner(deps.storage, info.sender.as_str())?, - ContractError::Unauthorized {} - ); - // If start time wasn't provided, it will be set as the current_time - let (start_expiration, _current_time) = get_and_validate_start_time(&env, start_time)?; - - let end_expiration = expiration_from_milliseconds(end_time.get_time(&env.block))?; - - ensure!( - end_expiration > start_expiration, - ContractError::StartTimeAfterEndTime {} - ); - - SALE_CONDUCTED.save(deps.storage, &true)?; - let state = STATE.may_load(deps.storage)?; - ensure!(state.is_none(), ContractError::SaleStarted {}); - let max_amount_per_wallet = max_amount_per_wallet.unwrap_or(1u32); - - // This is to prevent cloning price. - let price_str = price.to_string(); - STATE.save( - deps.storage, - &State { - end_time: end_expiration, - price, - min_tokens_sold, - max_amount_per_wallet, - amount_sold: Uint128::zero(), - amount_to_send: Uint128::zero(), - amount_transferred: Uint128::zero(), - recipient, - }, - )?; - - SALE_CONDUCTED.save(deps.storage, &true)?; - - Ok(Response::new() - .add_attribute("action", "start_sale") - .add_attribute("start_time", start_expiration.to_string()) - .add_attribute("end_time", end_expiration.to_string()) - .add_attribute("price", price_str) - .add_attribute("min_tokens_sold", min_tokens_sold) - .add_attribute("max_amount_per_wallet", max_amount_per_wallet.to_string())) -} - -fn execute_purchase_by_token_id( - ctx: ExecuteContext, - token_id: String, -) -> Result { - let ExecuteContext { - mut deps, - info, - env, - .. - } = ctx; - let sender = info.sender.to_string(); - let state = STATE.may_load(deps.storage)?; - - // CHECK :: That there is an ongoing sale. - ensure!(state.is_some(), ContractError::NoOngoingSale {}); - - let mut state = state.unwrap(); - ensure!( - !state.end_time.is_expired(&env.block), - ContractError::NoOngoingSale {} - ); - - let mut purchases = PURCHASES - .may_load(deps.storage, &sender)? - .unwrap_or_default(); - - ensure!( - AVAILABLE_TOKENS.has(deps.storage, &token_id), - ContractError::TokenNotAvailable {} - ); - - let max_possible = state.max_amount_per_wallet - purchases.len() as u32; - - // CHECK :: The user is able to purchase these without going over the limit. - ensure!(max_possible > 0, ContractError::PurchaseLimitReached {}); - - purchase_tokens( - &mut deps, - vec![token_id.clone()], - &info, - &mut state, - &mut purchases, - )?; - - STATE.save(deps.storage, &state)?; - PURCHASES.save(deps.storage, &sender, &purchases)?; - - Ok(Response::new() - .add_attribute("action", "purchase") - .add_attribute("token_id", token_id)) -} - -fn execute_purchase( - ctx: ExecuteContext, - number_of_tokens: Option, -) -> Result { - let ExecuteContext { - mut deps, - info, - env, - .. - } = ctx; - let sender = info.sender.to_string(); - let state = STATE.may_load(deps.storage)?; - - // CHECK :: That there is an ongoing sale. - ensure!(state.is_some(), ContractError::NoOngoingSale {}); - - let mut state = state.unwrap(); - ensure!( - !state.end_time.is_expired(&env.block), - ContractError::NoOngoingSale {} - ); - - let mut purchases = PURCHASES - .may_load(deps.storage, &sender)? - .unwrap_or_default(); - - let max_possible = state.max_amount_per_wallet - purchases.len() as u32; - - // CHECK :: The user is able to purchase these without going over the limit. - ensure!(max_possible > 0, ContractError::PurchaseLimitReached {}); - - let number_of_tokens_wanted = - number_of_tokens.map_or(max_possible, |n| cmp::min(n, max_possible)); - - // The number of token ids here is equal to min(number_of_tokens_wanted, num_tokens_left). - let token_ids = get_available_tokens(deps.storage, None, Some(number_of_tokens_wanted))?; - - let number_of_tokens_purchased = token_ids.len(); - - let required_payment = - purchase_tokens(&mut deps, token_ids, &info, &mut state, &mut purchases)?; - - PURCHASES.save(deps.storage, &sender, &purchases)?; - STATE.save(deps.storage, &state)?; - - // Refund user if they sent more. This can happen near the end of the sale when they weren't - // able to get the amount that they wanted. - let mut funds = info.funds; - deduct_funds(&mut funds, &required_payment)?; - - // If any funds were remaining after deduction, send refund. - let resp = if has_coins(&funds, &Coin::new(1, state.price.denom)) { - Response::new().add_message(BankMsg::Send { - to_address: sender, - amount: funds, - }) - } else { - Response::new() - }; - - Ok(resp - .add_attribute("action", "purchase") - .add_attribute( - "number_of_tokens_wanted", - number_of_tokens_wanted.to_string(), - ) - .add_attribute( - "number_of_tokens_purchased", - number_of_tokens_purchased.to_string(), - )) -} - -fn purchase_tokens( - deps: &mut DepsMut, - token_ids: Vec, - info: &MessageInfo, - state: &mut State, - purchases: &mut Vec, -) -> Result { - // CHECK :: There are any tokens left to purchase. - ensure!(!token_ids.is_empty(), ContractError::AllTokensPurchased {}); - - let number_of_tokens_purchased = token_ids.len(); - - // CHECK :: The user has sent enough funds to cover the base fee (without any taxes). - let total_cost = Coin::new( - state.price.amount.u128() * number_of_tokens_purchased as u128, - state.price.denom.clone(), - ); - ensure!( - has_coins(&info.funds, &total_cost), - ContractError::InsufficientFunds {} - ); - - let mut total_tax_amount = Uint128::zero(); - - // This is the same for each token, so we only need to do it once. - let (msgs, _events, remainder) = ADOContract::default().on_funds_transfer( - &deps.as_ref(), - info.sender.to_string(), - Funds::Native(state.price.clone()), - encode_binary(&"")?, - )?; - - let mut current_number = NUMBER_OF_TOKENS_AVAILABLE.load(deps.storage)?; - for token_id in token_ids { - let remaining_amount = remainder.try_get_coin()?; - - let tax_amount = get_tax_amount(&msgs, state.price.amount, remaining_amount.amount); - - let purchase = Purchase { - token_id: token_id.clone(), - tax_amount, - msgs: msgs.clone(), - purchaser: info.sender.to_string(), - }; - total_tax_amount = total_tax_amount.checked_add(tax_amount)?; - - state.amount_to_send = state.amount_to_send.checked_add(remaining_amount.amount)?; - state.amount_sold = state.amount_sold.checked_add(Uint128::one())?; - - purchases.push(purchase); - - AVAILABLE_TOKENS.remove(deps.storage, &token_id); - current_number = current_number.checked_sub(Uint128::one())?; - } - NUMBER_OF_TOKENS_AVAILABLE.save(deps.storage, ¤t_number)?; - - // CHECK :: User has sent enough to cover taxes. - let required_payment = Coin { - denom: state.price.denom.clone(), - amount: state - .price - .amount - .checked_mul(Uint128::from(number_of_tokens_purchased as u128))? - .checked_add(total_tax_amount)?, - }; - ensure!( - has_coins(&info.funds, &required_payment), - ContractError::InsufficientFunds {} - ); - Ok(required_payment) -} - -fn execute_claim_refund(ctx: ExecuteContext) -> Result { - let ExecuteContext { - deps, info, env, .. - } = ctx; - nonpayable(&info)?; - - let state = STATE.may_load(deps.storage)?; - ensure!(state.is_some(), ContractError::NoOngoingSale {}); - let state = state.unwrap(); - ensure!( - state.end_time.is_expired(&env.block), - ContractError::SaleNotEnded {} - ); - ensure!( - state.amount_sold < state.min_tokens_sold, - ContractError::MinSalesExceeded {} - ); - - let purchases = PURCHASES.may_load(deps.storage, info.sender.as_str())?; - ensure!(purchases.is_some(), ContractError::NoPurchases {}); - let purchases = purchases.unwrap(); - let refund_msg = process_refund(deps.storage, &purchases, &state.price); - let mut resp = Response::new(); - if let Some(refund_msg) = refund_msg { - resp = resp.add_message(refund_msg); - } - - Ok(resp.add_attribute("action", "claim_refund")) -} - -fn execute_end_sale(ctx: ExecuteContext, limit: Option) -> Result { - let ExecuteContext { - mut deps, - info, - env, - amp_ctx, - } = ctx; - nonpayable(&info)?; - - let state = STATE.may_load(deps.storage)?; - ensure!(state.is_some(), ContractError::NoOngoingSale {}); - let state = state.unwrap(); - let number_of_tokens_available = NUMBER_OF_TOKENS_AVAILABLE.load(deps.storage)?; - // In case the minimum sold tokens threshold is met, it has to be the owner who calls the function - let contract = ADOContract::default(); - let has_minimum_sold = state.min_tokens_sold <= state.amount_sold; - let is_owner = contract.is_contract_owner(deps.storage, info.sender.as_str())?; - - ensure!( - // If all tokens have been sold the sale can be ended too. - state.end_time.is_expired(&env.block) - || number_of_tokens_available.is_zero() - || (has_minimum_sold && is_owner), - ContractError::SaleNotEnded {} - ); - if state.amount_sold < state.min_tokens_sold { - issue_refunds_and_burn_tokens(&mut deps, env, limit) - } else { - transfer_tokens_and_send_funds( - ExecuteContext { - deps, - info, - env, - amp_ctx, - }, - limit, - ) - } -} - -fn issue_refunds_and_burn_tokens( - deps: &mut DepsMut, - env: Env, - limit: Option, -) -> Result { - let state = STATE.load(deps.storage)?; - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - ensure!(limit > 0, ContractError::LimitMustNotBeZero {}); - let mut refund_msgs: Vec = vec![]; - // Issue refunds for `limit` number of users. - let purchases: Vec> = PURCHASES - .range(deps.storage, None, None, Order::Ascending) - .take(limit) - .flatten() - .map(|(_v, p)| p) - .collect(); - for purchase_vec in purchases.iter() { - let refund_msg = process_refund(deps.storage, purchase_vec, &state.price); - if let Some(refund_msg) = refund_msg { - refund_msgs.push(refund_msg); - } - } - - // Burn `limit` number of tokens - let burn_msgs = get_burn_messages(deps, env.contract.address.to_string(), limit)?; - if burn_msgs.is_empty() && purchases.is_empty() { - // When all tokens have been burned and all purchases have been refunded, the sale is over. - clear_state(deps.storage)?; - } - - Ok(Response::new() - .add_attribute("action", "issue_refunds_and_burn_tokens") - .add_messages(refund_msgs) - .add_messages(burn_msgs)) -} - -fn transfer_tokens_and_send_funds( - ctx: ExecuteContext, - limit: Option, -) -> Result { - let ExecuteContext { - mut deps, - info, - env, - .. - } = ctx; - let mut state = STATE.load(deps.storage)?; - let mut resp = Response::new(); - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - - ensure!(limit > 0, ContractError::LimitMustNotBeZero {}); - // Send the funds if they haven't been sent yet and if all of the tokens have been transferred. - let mut pkt = match ctx.amp_ctx { - Some(pkt) => pkt, - None => AMPPkt::new(info.sender, env.contract.address.clone(), vec![]), - }; - - if state.amount_transferred == state.amount_sold { - if state.amount_to_send > Uint128::zero() { - let funds = vec![Coin { - denom: state.price.denom.clone(), - amount: state.amount_to_send, - }]; - match state.recipient.msg { - None => { - resp = resp.add_submessage( - state.recipient.generate_direct_msg(&deps.as_ref(), funds)?, - ); - } - Some(_) => { - let amp_message = state - .recipient - .generate_amp_msg(&deps.as_ref(), Some(funds)) - .unwrap(); - pkt = pkt.add_message(amp_message); - let kernel_address = ADOContract::default().get_kernel_address(deps.storage)?; - let sub_msg = pkt.to_sub_msg( - kernel_address, - Some(coins( - state.amount_to_send.u128(), - state.price.denom.clone(), - )), - 1, - )?; - resp = resp.add_submessage(sub_msg); - } - } - state.amount_to_send = Uint128::zero(); - STATE.save(deps.storage, &state)?; - } - // Once all purchased tokens have been transferred, begin burning `limit` number of tokens - // that were not purchased. - let burn_msgs = get_burn_messages(&mut deps, env.contract.address.to_string(), limit)?; - - if burn_msgs.is_empty() { - // When burn messages are empty, we have finished the sale, which is represented by - // having no State. - clear_state(deps.storage)?; - } else { - resp = resp.add_messages(burn_msgs); - } - // If we are here then there are no purchases to process so we can exit. - return Ok(resp.add_attribute("action", "transfer_tokens_and_send_funds")); - } - let mut purchases: Vec = PURCHASES - .range(deps.storage, None, None, Order::Ascending) - .flatten() - // Flatten Vec> into Vec. - .flat_map(|(_v, p)| p) - // Take one extra in order to compare what the next purchaser would be to check if some - // purchases will be left over. - .take(limit + 1) - .collect(); - - let config = CONFIG.load(deps.storage)?; - let mut rate_messages: Vec = vec![]; - let mut transfer_msgs: Vec = vec![]; - - let last_purchaser = if purchases.len() == 1 { - purchases[0].purchaser.clone() - } else { - purchases[purchases.len() - 2].purchaser.clone() - }; - // This subtraction is no problem as we will always have at least one purchase. - let subsequent_purchase = &purchases[purchases.len() - 1]; - // If this is false, then there are some purchases that we will need to leave for the next - // round. Otherwise, we are able to process all of the purchases for the last purchaser and we - // can remove their entry from the map entirely. - let remove_last_purchaser = last_purchaser != subsequent_purchase.purchaser; - - let mut number_of_last_purchases_removed = 0; - // If we took an extra element, we remove it. Otherwise limit + 1 was more than was necessary - // so we need to remove all of the purchases from the map. - if limit + 1 == purchases.len() { - // This is an O(1) operation from looking at the source code. - purchases.pop(); - } - - // Resolve the token contract address from the VFS - let token_contract_address = config.token_address.get_raw_address(&deps.as_ref())?; - for purchase in purchases.into_iter() { - let purchaser = purchase.purchaser; - let should_remove = purchaser != last_purchaser || remove_last_purchaser; - if should_remove && PURCHASES.has(deps.storage, &purchaser) { - PURCHASES.remove(deps.storage, &purchaser); - } else if purchaser == last_purchaser { - // Keep track of the number of purchases removed from the last purchaser to remove them - // at the end, if not all of them were removed. - number_of_last_purchases_removed += 1; - } - rate_messages.extend(purchase.msgs); - transfer_msgs.push(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: token_contract_address.to_string(), - msg: encode_binary(&Cw721ExecuteMsg::TransferNft { - recipient: AndrAddr::from_string(purchaser), - token_id: purchase.token_id, - })?, - funds: vec![], - })); - state.amount_transferred = state.amount_transferred.checked_add(Uint128::one())?; - } - // If the last purchaser wasn't removed, remove the subset of purchases that were processed. - if PURCHASES.has(deps.storage, &last_purchaser) { - let last_purchases = PURCHASES.load(deps.storage, &last_purchaser)?; - PURCHASES.save( - deps.storage, - &last_purchaser, - &last_purchases[number_of_last_purchases_removed..].to_vec(), - )?; - } - STATE.save(deps.storage, &state)?; - - Ok(resp - .add_attribute("action", "transfer_tokens_and_send_funds") - .add_messages(transfer_msgs) - .add_submessages(merge_sub_msgs(rate_messages))) -} - -/// Processes a vector of purchases for the SAME user by merging all funds into a single BankMsg. -/// The given purchaser is then removed from `PURCHASES`. -/// -/// ## Arguments -/// * `storage` - Mutable reference to Storage -/// * `purchase` - Vector of purchases for the same user to issue a refund message for. -/// * `price` - The price of a token -/// -/// Returns an `Option` which is `None` when the amount to refund is zero. -fn process_refund( - storage: &mut dyn Storage, - purchases: &[Purchase], - price: &Coin, -) -> Option { - let purchaser = purchases[0].purchaser.clone(); - // Remove each entry as they get processed. - PURCHASES.remove(storage, &purchaser); - // Reduce a user's purchases into one message. While the tax paid on each item should - // be the same, it is not guaranteed given that the rates module is mutable during the - // sale. - let amount = purchases - .iter() - // This represents the total amount of funds they sent for each purchase. - .map(|p| p.tax_amount + price.amount) - // Adds up all of the purchases. - .reduce(|accum, item| accum + item) - .unwrap_or_else(Uint128::zero); - - if amount > Uint128::zero() { - Some(CosmosMsg::Bank(BankMsg::Send { - to_address: purchaser, - amount: vec![Coin { - denom: price.denom.clone(), - amount, - }], - })) - } else { - None - } -} - -fn get_burn_messages( - deps: &mut DepsMut, - address: String, - limit: usize, -) -> Result, ContractError> { - let config = CONFIG.load(deps.storage)?; - let token_address = config.token_address.get_raw_address(&deps.as_ref())?; - let tokens_to_burn = query_tokens(&deps.querier, token_address.to_string(), address, limit)?; - - tokens_to_burn - .into_iter() - .map(|token_id| { - // Any token that is burnable has been added to this map, and so must be removed. - AVAILABLE_TOKENS.remove(deps.storage, &token_id); - Ok(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: token_address.to_string(), - funds: vec![], - msg: encode_binary(&Cw721ExecuteMsg::Burn { token_id })?, - })) - }) - .collect() -} - -fn clear_state(storage: &mut dyn Storage) -> Result<(), ContractError> { - STATE.remove(storage); - NUMBER_OF_TOKENS_AVAILABLE.save(storage, &Uint128::zero())?; - - Ok(()) -} - -fn query_tokens( - querier: &QuerierWrapper, - token_address: String, - owner: String, - limit: usize, -) -> Result, ContractError> { - let res: TokensResponse = querier.query(&QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: token_address, - msg: encode_binary(&Cw721QueryMsg::Tokens { - owner, - start_after: None, - limit: Some(limit as u32), - })?, - }))?; - Ok(res.tokens) + // let res = match msg { + // ExecuteMsg::Mint(mint_msgs) => execute_mint(ctx, mint_msgs), + // ExecuteMsg::StartSale { + // start_time, + // end_time, + // price, + // min_tokens_sold, + // max_amount_per_wallet, + // recipient, + // } => execute_start_sale( + // ctx, + // start_time, + // end_time, + // price, + // min_tokens_sold, + // max_amount_per_wallet, + // recipient, + // ), + // ExecuteMsg::Purchase { number_of_tokens } => execute_purchase(ctx, number_of_tokens), + // ExecuteMsg::PurchaseByTokenId { token_id } => execute_purchase_by_token_id(ctx, token_id), + // ExecuteMsg::ClaimRefund {} => execute_claim_refund(ctx), + // ExecuteMsg::EndSale { limit } => execute_end_sale(ctx, limit), + // ExecuteMsg::UpdateTokenContract { address } => execute_update_token_contract(ctx, address), + // _ => ADOContract::default().execute(ctx, msg), + // }?; } #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { - match msg { - QueryMsg::State {} => encode_binary(&query_state(deps)?), - QueryMsg::Config {} => encode_binary(&query_config(deps)?), - QueryMsg::AvailableTokens { start_after, limit } => { - encode_binary(&query_available_tokens(deps, start_after, limit)?) - } - QueryMsg::IsTokenAvailable { id } => encode_binary(&query_is_token_available(deps, id)), - _ => ADOContract::default().query(deps, env, msg), - } -} - -fn query_state(deps: Deps) -> Result { - Ok(STATE.load(deps.storage)?) -} - -fn query_config(deps: Deps) -> Result { - Ok(CONFIG.load(deps.storage)?) -} - -fn query_available_tokens( - deps: Deps, - start_after: Option, - limit: Option, -) -> Result, ContractError> { - get_available_tokens(deps.storage, start_after, limit) + ADOContract::default().query(deps, env, msg) + // match msg { + // QueryMsg::State {} => encode_binary(&query_state(deps)?), + // QueryMsg::Config {} => encode_binary(&query_config(deps)?), + // QueryMsg::AvailableTokens { start_after, limit } => { + // encode_binary(&query_available_tokens(deps, start_after, limit)?) + // } + // QueryMsg::IsTokenAvailable { id } => encode_binary(&query_is_token_available(deps, id)), + // _ => ADOContract::default().query(deps, env, msg), + // } } -fn query_is_token_available(deps: Deps, id: String) -> IsTokenAvailableResponse { - IsTokenAvailableResponse { - is_token_available: AVAILABLE_TOKENS.has(deps.storage, &id), - } -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { - ADOContract::default().migrate(deps, CONTRACT_NAME, CONTRACT_VERSION) -} +// fn execute_mint( +// ctx: ExecuteContext, +// mint_msgs: Vec, +// ) -> Result { +// let ExecuteContext { +// deps, info, env, .. +// } = ctx; +// nonpayable(&info)?; + +// ensure!( +// mint_msgs.len() <= MAX_MINT_LIMIT as usize, +// ContractError::TooManyMintMessages { +// limit: MAX_MINT_LIMIT, +// } +// ); +// let contract = ADOContract::default(); +// ensure!( +// contract.is_contract_owner(deps.storage, info.sender.as_str())?, +// ContractError::Unauthorized {} +// ); +// // Can only mint when no sale is ongoing. +// ensure!( +// STATE.may_load(deps.storage)?.is_none(), +// ContractError::SaleStarted {} +// ); +// let sale_conducted = SALE_CONDUCTED.load(deps.storage)?; +// let config = CONFIG.load(deps.storage)?; +// ensure!( +// config.can_mint_after_sale || !sale_conducted, +// ContractError::CannotMintAfterSaleConducted {} +// ); + +// let token_contract = config.token_address; +// let crowdfund_contract = env.contract.address.to_string(); +// let resolved_path = token_contract.get_raw_address(&deps.as_ref())?; + +// let mut resp = Response::new(); +// for mint_msg in mint_msgs { +// let mint_resp = mint( +// deps.storage, +// &crowdfund_contract, +// resolved_path.to_string(), +// mint_msg, +// )?; +// resp = resp +// .add_attributes(mint_resp.attributes) +// .add_submessages(mint_resp.messages); +// } + +// Ok(resp) +// } + +// fn mint( +// storage: &mut dyn Storage, +// crowdfund_contract: &str, +// token_contract: String, +// mint_msg: CrowdfundMintMsg, +// ) -> Result { +// let mint_msg: MintMsg = MintMsg { +// token_id: mint_msg.token_id, +// owner: mint_msg +// .owner +// .unwrap_or_else(|| crowdfund_contract.to_owned()), +// token_uri: mint_msg.token_uri, +// extension: mint_msg.extension, +// }; +// // We allow for owners other than the contract, incase the creator wants to set aside a few +// // tokens for some other use, say airdrop, team allocation, etc. Only those which have the +// // contract as the owner will be available to sell. +// if mint_msg.owner == crowdfund_contract { +// // Mark token as available to purchase in next sale. +// AVAILABLE_TOKENS.save(storage, &mint_msg.token_id, &true)?; +// let current_number = NUMBER_OF_TOKENS_AVAILABLE.load(storage)?; +// NUMBER_OF_TOKENS_AVAILABLE.save(storage, &(current_number + Uint128::new(1)))?; +// } +// Ok(Response::new() +// .add_attribute("action", "mint") +// .add_message(WasmMsg::Execute { +// contract_addr: token_contract, +// msg: encode_binary(&Cw721ExecuteMsg::Mint { +// token_id: mint_msg.token_id, +// owner: mint_msg.owner, +// token_uri: mint_msg.token_uri, +// extension: mint_msg.extension, +// })?, +// funds: vec![], +// })) +// } + +// fn execute_update_token_contract( +// ctx: ExecuteContext, +// address: AndrAddr, +// ) -> Result { +// let ExecuteContext { deps, info, .. } = ctx; +// nonpayable(&info)?; + +// let contract = ADOContract::default(); +// ensure!( +// contract.is_contract_owner(deps.storage, info.sender.as_str())?, +// ContractError::Unauthorized {} +// ); +// // Ensure no tokens have been minted already +// let num_tokens = NUMBER_OF_TOKENS_AVAILABLE +// .load(deps.storage) +// .unwrap_or(Uint128::zero()); +// ensure!(num_tokens.is_zero(), ContractError::Unauthorized {}); + +// // Will error if not a valid path +// let addr = address.get_raw_address(&deps.as_ref())?; +// let query = Cw721QueryMsg::ContractInfo {}; + +// // Check contract is a valid CW721 contract +// let res: Result = deps.querier.query_wasm_smart(addr, &query); +// ensure!(res.is_ok(), ContractError::Unauthorized {}); + +// CONFIG.update(deps.storage, |mut config| { +// config.token_address = address; +// Ok::<_, ContractError>(config) +// })?; +// Ok(Response::new().add_attribute("action", "update_token_contract")) +// } + +// #[allow(clippy::too_many_arguments)] +// fn execute_start_sale( +// ctx: ExecuteContext, +// start_time: Option, +// end_time: Expiry, +// price: Coin, +// min_tokens_sold: Uint128, +// max_amount_per_wallet: Option, +// recipient: Recipient, +// ) -> Result { +// let ExecuteContext { +// deps, info, env, .. +// } = ctx; +// validate_denom(deps.as_ref(), price.denom.clone())?; +// recipient.validate(&deps.as_ref())?; +// nonpayable(&info)?; +// let ado_contract = ADOContract::default(); + +// // Validate recipient +// ado_contract.validate_andr_addresses(&deps.as_ref(), vec![recipient.address.clone()])?; +// ensure!( +// ADOContract::default().is_contract_owner(deps.storage, info.sender.as_str())?, +// ContractError::Unauthorized {} +// ); +// // If start time wasn't provided, it will be set as the current_time +// let (start_expiration, _current_time) = get_and_validate_start_time(&env, start_time)?; + +// let end_expiration = expiration_from_milliseconds(end_time.get_time(&env.block))?; + +// ensure!( +// end_expiration > start_expiration, +// ContractError::StartTimeAfterEndTime {} +// ); + +// SALE_CONDUCTED.save(deps.storage, &true)?; +// let state = STATE.may_load(deps.storage)?; +// ensure!(state.is_none(), ContractError::SaleStarted {}); +// let max_amount_per_wallet = max_amount_per_wallet.unwrap_or(1u32); + +// // This is to prevent cloning price. +// let price_str = price.to_string(); +// STATE.save( +// deps.storage, +// &State { +// end_time: end_expiration, +// price, +// min_tokens_sold, +// max_amount_per_wallet, +// amount_sold: Uint128::zero(), +// amount_to_send: Uint128::zero(), +// amount_transferred: Uint128::zero(), +// recipient, +// }, +// )?; + +// SALE_CONDUCTED.save(deps.storage, &true)?; + +// Ok(Response::new() +// .add_attribute("action", "start_sale") +// .add_attribute("start_time", start_expiration.to_string()) +// .add_attribute("end_time", end_expiration.to_string()) +// .add_attribute("price", price_str) +// .add_attribute("min_tokens_sold", min_tokens_sold) +// .add_attribute("max_amount_per_wallet", max_amount_per_wallet.to_string())) +// } + +// fn execute_purchase_by_token_id( +// ctx: ExecuteContext, +// token_id: String, +// ) -> Result { +// let ExecuteContext { +// mut deps, +// info, +// env, +// .. +// } = ctx; +// let sender = info.sender.to_string(); +// let state = STATE.may_load(deps.storage)?; + +// // CHECK :: That there is an ongoing sale. +// ensure!(state.is_some(), ContractError::NoOngoingSale {}); + +// let mut state = state.unwrap(); +// ensure!( +// !state.end_time.is_expired(&env.block), +// ContractError::NoOngoingSale {} +// ); + +// let mut purchases = PURCHASES +// .may_load(deps.storage, &sender)? +// .unwrap_or_default(); + +// ensure!( +// AVAILABLE_TOKENS.has(deps.storage, &token_id), +// ContractError::TokenNotAvailable {} +// ); + +// let max_possible = state.max_amount_per_wallet - purchases.len() as u32; + +// // CHECK :: The user is able to purchase these without going over the limit. +// ensure!(max_possible > 0, ContractError::PurchaseLimitReached {}); + +// purchase_tokens( +// &mut deps, +// vec![token_id.clone()], +// &info, +// &mut state, +// &mut purchases, +// )?; + +// STATE.save(deps.storage, &state)?; +// PURCHASES.save(deps.storage, &sender, &purchases)?; + +// Ok(Response::new() +// .add_attribute("action", "purchase") +// .add_attribute("token_id", token_id)) +// } + +// fn execute_purchase( +// ctx: ExecuteContext, +// number_of_tokens: Option, +// ) -> Result { +// let ExecuteContext { +// mut deps, +// info, +// env, +// .. +// } = ctx; +// let sender = info.sender.to_string(); +// let state = STATE.may_load(deps.storage)?; + +// // CHECK :: That there is an ongoing sale. +// ensure!(state.is_some(), ContractError::NoOngoingSale {}); + +// let mut state = state.unwrap(); +// ensure!( +// !state.end_time.is_expired(&env.block), +// ContractError::NoOngoingSale {} +// ); + +// let mut purchases = PURCHASES +// .may_load(deps.storage, &sender)? +// .unwrap_or_default(); + +// let max_possible = state.max_amount_per_wallet - purchases.len() as u32; + +// // CHECK :: The user is able to purchase these without going over the limit. +// ensure!(max_possible > 0, ContractError::PurchaseLimitReached {}); + +// let number_of_tokens_wanted = +// number_of_tokens.map_or(max_possible, |n| cmp::min(n, max_possible)); + +// // The number of token ids here is equal to min(number_of_tokens_wanted, num_tokens_left). +// let token_ids = get_available_tokens(deps.storage, None, Some(number_of_tokens_wanted))?; + +// let number_of_tokens_purchased = token_ids.len(); + +// let required_payment = +// purchase_tokens(&mut deps, token_ids, &info, &mut state, &mut purchases)?; + +// PURCHASES.save(deps.storage, &sender, &purchases)?; +// STATE.save(deps.storage, &state)?; + +// // Refund user if they sent more. This can happen near the end of the sale when they weren't +// // able to get the amount that they wanted. +// let mut funds = info.funds; +// deduct_funds(&mut funds, &required_payment)?; + +// // If any funds were remaining after deduction, send refund. +// let resp = if has_coins(&funds, &Coin::new(1, state.price.denom)) { +// Response::new().add_message(BankMsg::Send { +// to_address: sender, +// amount: funds, +// }) +// } else { +// Response::new() +// }; + +// Ok(resp +// .add_attribute("action", "purchase") +// .add_attribute( +// "number_of_tokens_wanted", +// number_of_tokens_wanted.to_string(), +// ) +// .add_attribute( +// "number_of_tokens_purchased", +// number_of_tokens_purchased.to_string(), +// )) +// } + +// fn purchase_tokens( +// deps: &mut DepsMut, +// token_ids: Vec, +// info: &MessageInfo, +// state: &mut State, +// purchases: &mut Vec, +// ) -> Result { +// // CHECK :: There are any tokens left to purchase. +// ensure!(!token_ids.is_empty(), ContractError::AllTokensPurchased {}); + +// let number_of_tokens_purchased = token_ids.len(); + +// // CHECK :: The user has sent enough funds to cover the base fee (without any taxes). +// let total_cost = Coin::new( +// state.price.amount.u128() * number_of_tokens_purchased as u128, +// state.price.denom.clone(), +// ); +// ensure!( +// has_coins(&info.funds, &total_cost), +// ContractError::InsufficientFunds {} +// ); + +// let mut total_tax_amount = Uint128::zero(); + +// // This is the same for each token, so we only need to do it once. +// let (msgs, _events, remainder) = ADOContract::default().on_funds_transfer( +// &deps.as_ref(), +// info.sender.to_string(), +// Funds::Native(state.price.clone()), +// encode_binary(&"")?, +// )?; + +// let mut current_number = NUMBER_OF_TOKENS_AVAILABLE.load(deps.storage)?; +// for token_id in token_ids { +// let remaining_amount = remainder.try_get_coin()?; + +// let tax_amount = get_tax_amount(&msgs, state.price.amount, remaining_amount.amount); + +// let purchase = Purchase { +// token_id: token_id.clone(), +// tax_amount, +// msgs: msgs.clone(), +// purchaser: info.sender.to_string(), +// }; +// total_tax_amount = total_tax_amount.checked_add(tax_amount)?; + +// state.amount_to_send = state.amount_to_send.checked_add(remaining_amount.amount)?; +// state.amount_sold = state.amount_sold.checked_add(Uint128::one())?; + +// purchases.push(purchase); + +// AVAILABLE_TOKENS.remove(deps.storage, &token_id); +// current_number = current_number.checked_sub(Uint128::one())?; +// } +// NUMBER_OF_TOKENS_AVAILABLE.save(deps.storage, ¤t_number)?; + +// // CHECK :: User has sent enough to cover taxes. +// let required_payment = Coin { +// denom: state.price.denom.clone(), +// amount: state +// .price +// .amount +// .checked_mul(Uint128::from(number_of_tokens_purchased as u128))? +// .checked_add(total_tax_amount)?, +// }; +// ensure!( +// has_coins(&info.funds, &required_payment), +// ContractError::InsufficientFunds {} +// ); +// Ok(required_payment) +// } + +// fn execute_claim_refund(ctx: ExecuteContext) -> Result { +// let ExecuteContext { +// deps, info, env, .. +// } = ctx; +// nonpayable(&info)?; + +// let state = STATE.may_load(deps.storage)?; +// ensure!(state.is_some(), ContractError::NoOngoingSale {}); +// let state = state.unwrap(); +// ensure!( +// state.end_time.is_expired(&env.block), +// ContractError::SaleNotEnded {} +// ); +// ensure!( +// state.amount_sold < state.min_tokens_sold, +// ContractError::MinSalesExceeded {} +// ); + +// let purchases = PURCHASES.may_load(deps.storage, info.sender.as_str())?; +// ensure!(purchases.is_some(), ContractError::NoPurchases {}); +// let purchases = purchases.unwrap(); +// let refund_msg = process_refund(deps.storage, &purchases, &state.price); +// let mut resp = Response::new(); +// if let Some(refund_msg) = refund_msg { +// resp = resp.add_message(refund_msg); +// } + +// Ok(resp.add_attribute("action", "claim_refund")) +// } + +// fn execute_end_sale(ctx: ExecuteContext, limit: Option) -> Result { +// let ExecuteContext { +// mut deps, +// info, +// env, +// amp_ctx, +// } = ctx; +// nonpayable(&info)?; + +// let state = STATE.may_load(deps.storage)?; +// ensure!(state.is_some(), ContractError::NoOngoingSale {}); +// let state = state.unwrap(); +// let number_of_tokens_available = NUMBER_OF_TOKENS_AVAILABLE.load(deps.storage)?; +// // In case the minimum sold tokens threshold is met, it has to be the owner who calls the function +// let contract = ADOContract::default(); +// let has_minimum_sold = state.min_tokens_sold <= state.amount_sold; +// let is_owner = contract.is_contract_owner(deps.storage, info.sender.as_str())?; + +// ensure!( +// // If all tokens have been sold the sale can be ended too. +// state.end_time.is_expired(&env.block) +// || number_of_tokens_available.is_zero() +// || (has_minimum_sold && is_owner), +// ContractError::SaleNotEnded {} +// ); +// if state.amount_sold < state.min_tokens_sold { +// issue_refunds_and_burn_tokens(&mut deps, env, limit) +// } else { +// transfer_tokens_and_send_funds( +// ExecuteContext { +// deps, +// info, +// env, +// amp_ctx, +// }, +// limit, +// ) +// } +// } + +// fn issue_refunds_and_burn_tokens( +// deps: &mut DepsMut, +// env: Env, +// limit: Option, +// ) -> Result { +// let state = STATE.load(deps.storage)?; +// let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; +// ensure!(limit > 0, ContractError::LimitMustNotBeZero {}); +// let mut refund_msgs: Vec = vec![]; +// // Issue refunds for `limit` number of users. +// let purchases: Vec> = PURCHASES +// .range(deps.storage, None, None, Order::Ascending) +// .take(limit) +// .flatten() +// .map(|(_v, p)| p) +// .collect(); +// for purchase_vec in purchases.iter() { +// let refund_msg = process_refund(deps.storage, purchase_vec, &state.price); +// if let Some(refund_msg) = refund_msg { +// refund_msgs.push(refund_msg); +// } +// } + +// // Burn `limit` number of tokens +// let burn_msgs = get_burn_messages(deps, env.contract.address.to_string(), limit)?; + +// if burn_msgs.is_empty() && purchases.is_empty() { +// // When all tokens have been burned and all purchases have been refunded, the sale is over. +// clear_state(deps.storage)?; +// } + +// Ok(Response::new() +// .add_attribute("action", "issue_refunds_and_burn_tokens") +// .add_messages(refund_msgs) +// .add_messages(burn_msgs)) +// } + +// fn transfer_tokens_and_send_funds( +// ctx: ExecuteContext, +// limit: Option, +// ) -> Result { +// let ExecuteContext { +// mut deps, +// info, +// env, +// .. +// } = ctx; +// let mut state = STATE.load(deps.storage)?; +// let mut resp = Response::new(); +// let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + +// ensure!(limit > 0, ContractError::LimitMustNotBeZero {}); +// // Send the funds if they haven't been sent yet and if all of the tokens have been transferred. +// let mut pkt = match ctx.amp_ctx { +// Some(pkt) => pkt, +// None => AMPPkt::new(info.sender, env.contract.address.clone(), vec![]), +// }; + +// if state.amount_transferred == state.amount_sold { +// if state.amount_to_send > Uint128::zero() { +// let funds = vec![Coin { +// denom: state.price.denom.clone(), +// amount: state.amount_to_send, +// }]; +// match state.recipient.msg { +// None => { +// resp = resp.add_submessage( +// state.recipient.generate_direct_msg(&deps.as_ref(), funds)?, +// ); +// } +// Some(_) => { +// let amp_message = state +// .recipient +// .generate_amp_msg(&deps.as_ref(), Some(funds)) +// .unwrap(); +// pkt = pkt.add_message(amp_message); +// let kernel_address = ADOContract::default().get_kernel_address(deps.storage)?; +// let sub_msg = pkt.to_sub_msg( +// kernel_address, +// Some(coins( +// state.amount_to_send.u128(), +// state.price.denom.clone(), +// )), +// 1, +// )?; +// resp = resp.add_submessage(sub_msg); +// } +// } +// state.amount_to_send = Uint128::zero(); +// STATE.save(deps.storage, &state)?; +// } +// // Once all purchased tokens have been transferred, begin burning `limit` number of tokens +// // that were not purchased. +// let burn_msgs = get_burn_messages(&mut deps, env.contract.address.to_string(), limit)?; + +// if burn_msgs.is_empty() { +// // When burn messages are empty, we have finished the sale, which is represented by +// // having no State. +// clear_state(deps.storage)?; +// } else { +// resp = resp.add_messages(burn_msgs); +// } +// // If we are here then there are no purchases to process so we can exit. +// return Ok(resp.add_attribute("action", "transfer_tokens_and_send_funds")); +// } +// let mut purchases: Vec = PURCHASES +// .range(deps.storage, None, None, Order::Ascending) +// .flatten() +// // Flatten Vec> into Vec. +// .flat_map(|(_v, p)| p) +// // Take one extra in order to compare what the next purchaser would be to check if some +// // purchases will be left over. +// .take(limit + 1) +// .collect(); + +// let config = CONFIG.load(deps.storage)?; +// let mut rate_messages: Vec = vec![]; +// let mut transfer_msgs: Vec = vec![]; + +// let last_purchaser = if purchases.len() == 1 { +// purchases[0].purchaser.clone() +// } else { +// purchases[purchases.len() - 2].purchaser.clone() +// }; +// // This subtraction is no problem as we will always have at least one purchase. +// let subsequent_purchase = &purchases[purchases.len() - 1]; +// // If this is false, then there are some purchases that we will need to leave for the next +// // round. Otherwise, we are able to process all of the purchases for the last purchaser and we +// // can remove their entry from the map entirely. +// let remove_last_purchaser = last_purchaser != subsequent_purchase.purchaser; + +// let mut number_of_last_purchases_removed = 0; +// // If we took an extra element, we remove it. Otherwise limit + 1 was more than was necessary +// // so we need to remove all of the purchases from the map. +// if limit + 1 == purchases.len() { +// // This is an O(1) operation from looking at the source code. +// purchases.pop(); +// } + +// // Resolve the token contract address from the VFS +// let token_contract_address = config.token_address.get_raw_address(&deps.as_ref())?; +// for purchase in purchases.into_iter() { +// let purchaser = purchase.purchaser; +// let should_remove = purchaser != last_purchaser || remove_last_purchaser; +// if should_remove && PURCHASES.has(deps.storage, &purchaser) { +// PURCHASES.remove(deps.storage, &purchaser); +// } else if purchaser == last_purchaser { +// // Keep track of the number of purchases removed from the last purchaser to remove them +// // at the end, if not all of them were removed. +// number_of_last_purchases_removed += 1; +// } +// rate_messages.extend(purchase.msgs); +// transfer_msgs.push(CosmosMsg::Wasm(WasmMsg::Execute { +// contract_addr: token_contract_address.to_string(), +// msg: encode_binary(&Cw721ExecuteMsg::TransferNft { +// recipient: AndrAddr::from_string(purchaser), +// token_id: purchase.token_id, +// })?, +// funds: vec![], +// })); +// state.amount_transferred = state.amount_transferred.checked_add(Uint128::one())?; +// } +// // If the last purchaser wasn't removed, remove the subset of purchases that were processed. +// if PURCHASES.has(deps.storage, &last_purchaser) { +// let last_purchases = PURCHASES.load(deps.storage, &last_purchaser)?; +// PURCHASES.save( +// deps.storage, +// &last_purchaser, +// &last_purchases[number_of_last_purchases_removed..].to_vec(), +// )?; +// } +// STATE.save(deps.storage, &state)?; + +// Ok(resp +// .add_attribute("action", "transfer_tokens_and_send_funds") +// .add_messages(transfer_msgs) +// .add_submessages(merge_sub_msgs(rate_messages))) +// } + +// /// Processes a vector of purchases for the SAME user by merging all funds into a single BankMsg. +// /// The given purchaser is then removed from `PURCHASES`. +// /// +// /// ## Arguments +// /// * `storage` - Mutable reference to Storage +// /// * `purchase` - Vector of purchases for the same user to issue a refund message for. +// /// * `price` - The price of a token +// /// +// /// Returns an `Option` which is `None` when the amount to refund is zero. +// fn process_refund( +// storage: &mut dyn Storage, +// purchases: &[Purchase], +// price: &Coin, +// ) -> Option { +// let purchaser = purchases[0].purchaser.clone(); +// // Remove each entry as they get processed. +// PURCHASES.remove(storage, &purchaser); +// // Reduce a user's purchases into one message. While the tax paid on each item should +// // be the same, it is not guaranteed given that the rates module is mutable during the +// // sale. +// let amount = purchases +// .iter() +// // This represents the total amount of funds they sent for each purchase. +// .map(|p| p.tax_amount + price.amount) +// // Adds up all of the purchases. +// .reduce(|accum, item| accum + item) +// .unwrap_or_else(Uint128::zero); + +// if amount > Uint128::zero() { +// Some(CosmosMsg::Bank(BankMsg::Send { +// to_address: purchaser, +// amount: vec![Coin { +// denom: price.denom.clone(), +// amount, +// }], +// })) +// } else { +// None +// } +// } + +// fn get_burn_messages( +// deps: &mut DepsMut, +// address: String, +// limit: usize, +// ) -> Result, ContractError> { +// let config = CONFIG.load(deps.storage)?; +// let token_address = config.token_address.get_raw_address(&deps.as_ref())?; +// let tokens_to_burn = query_tokens(&deps.querier, token_address.to_string(), address, limit)?; + +// tokens_to_burn +// .into_iter() +// .map(|token_id| { +// // Any token that is burnable has been added to this map, and so must be removed. +// AVAILABLE_TOKENS.remove(deps.storage, &token_id); +// Ok(CosmosMsg::Wasm(WasmMsg::Execute { +// contract_addr: token_address.to_string(), +// funds: vec![], +// msg: encode_binary(&Cw721ExecuteMsg::Burn { token_id })?, +// })) +// }) +// .collect() +// } + +// fn clear_state(storage: &mut dyn Storage) -> Result<(), ContractError> { +// STATE.remove(storage); +// NUMBER_OF_TOKENS_AVAILABLE.save(storage, &Uint128::zero())?; + +// Ok(()) +// } + +// fn query_tokens( +// querier: &QuerierWrapper, +// token_address: String, +// owner: String, +// limit: usize, +// ) -> Result, ContractError> { +// let res: TokensResponse = querier.query(&QueryRequest::Wasm(WasmQuery::Smart { +// contract_addr: token_address, +// msg: encode_binary(&Cw721QueryMsg::Tokens { +// owner, +// start_after: None, +// limit: Some(limit as u32), +// })?, +// }))?; +// Ok(res.tokens) +// } + +// fn query_state(deps: Deps) -> Result { +// Ok(STATE.load(deps.storage)?) +// } + +// fn query_config(deps: Deps) -> Result { +// Ok(CONFIG.load(deps.storage)?) +// } + +// fn query_available_tokens( +// deps: Deps, +// start_after: Option, +// limit: Option, +// ) -> Result, ContractError> { +// get_available_tokens(deps.storage, start_after, limit) +// } + +// fn query_is_token_available(deps: Deps, id: String) -> IsTokenAvailableResponse { +// IsTokenAvailableResponse { +// is_token_available: AVAILABLE_TOKENS.has(deps.storage, &id), +// } +// } diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/mock.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/mock.rs index 78f129971..fee758a64 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/mock.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/mock.rs @@ -1,21 +1,16 @@ #![cfg(all(not(target_arch = "wasm32"), feature = "testing"))] use crate::contract::{execute, instantiate, query, reply}; -use andromeda_non_fungible_tokens::{ - crowdfund::{CrowdfundMintMsg, ExecuteMsg, InstantiateMsg, QueryMsg}, - cw721::TokenExtension, -}; -use andromeda_std::{ - ado_base::modules::Module, - amp::{AndrAddr, Recipient}, - common::expiration::Expiry, +use andromeda_non_fungible_tokens::crowdfund::{ + CampaignConfig, ExecuteMsg, InstantiateMsg, QueryMsg, }; +use andromeda_std::ado_base::modules::Module; use andromeda_testing::{ mock::MockApp, mock_ado, - mock_contract::{ExecuteResult, MockADO, MockContract}, + mock_contract::{MockADO, MockContract}, }; -use cosmwasm_std::{Addr, Coin, Empty, Uint128}; +use cosmwasm_std::{Addr, Empty}; use cw_multi_test::{Contract, ContractWrapper, Executor}; pub struct MockCrowdfund(Addr); @@ -27,19 +22,12 @@ impl MockCrowdfund { code_id: u64, sender: Addr, app: &mut MockApp, - token_address: AndrAddr, - can_mint_after_sale: bool, + campaign_config: CampaignConfig, modules: Option>, kernel_address: impl Into, owner: Option, ) -> MockCrowdfund { - let msg = mock_crowdfund_instantiate_msg( - token_address, - can_mint_after_sale, - modules, - kernel_address, - owner, - ); + let msg = mock_crowdfund_instantiate_msg(campaign_config, modules, kernel_address, owner); let addr = app .instantiate_contract( code_id, @@ -53,75 +41,75 @@ impl MockCrowdfund { MockCrowdfund(Addr::unchecked(addr)) } - #[allow(clippy::too_many_arguments)] - pub fn execute_start_sale( - &self, - sender: Addr, - app: &mut MockApp, - start_time: Option, - end_time: Expiry, - price: Coin, - min_tokens_sold: Uint128, - max_amount_per_wallet: Option, - recipient: Recipient, - ) -> ExecuteResult { - let msg = mock_start_crowdfund_msg( - start_time, - end_time, - price, - min_tokens_sold, - max_amount_per_wallet, - recipient, - ); - self.execute(app, &msg, sender, &[]) - } - - pub fn execute_end_sale( - &self, - sender: Addr, - app: &mut MockApp, - limit: Option, - ) -> ExecuteResult { - let msg = mock_end_crowdfund_msg(limit); - self.execute(app, &msg, sender, &[]) - } - - pub fn execute_mint( - &self, - sender: Addr, - app: &mut MockApp, - token_id: String, - extension: TokenExtension, - token_uri: Option, - owner: Option, - ) -> ExecuteResult { - let msg = ExecuteMsg::Mint(vec![mock_crowdfund_mint_msg( - token_id, extension, token_uri, owner, - )]); - self.execute(app, &msg, sender, &[]) - } - - pub fn execute_quick_mint( - &self, - sender: Addr, - app: &mut MockApp, - amount: u32, - publisher: String, - ) -> ExecuteResult { - let msg = mock_crowdfund_quick_mint_msg(amount, publisher); - self.execute(app, &msg, sender, &[]) - } - - pub fn execute_purchase( - &self, - sender: Addr, - app: &mut MockApp, - number_of_tokens: Option, - funds: &[Coin], - ) -> ExecuteResult { - let msg = mock_purchase_msg(number_of_tokens); - self.execute(app, &msg, sender, funds) - } + // #[allow(clippy::too_many_arguments)] + // pub fn execute_start_sale( + // &self, + // sender: Addr, + // app: &mut MockApp, + // start_time: Option, + // end_time: Expiry, + // price: Coin, + // min_tokens_sold: Uint128, + // max_amount_per_wallet: Option, + // recipient: Recipient, + // ) -> ExecuteResult { + // let msg = mock_start_crowdfund_msg( + // start_time, + // end_time, + // price, + // min_tokens_sold, + // max_amount_per_wallet, + // recipient, + // ); + // self.execute(app, &msg, sender, &[]) + // } + + // pub fn execute_end_sale( + // &self, + // sender: Addr, + // app: &mut MockApp, + // limit: Option, + // ) -> ExecuteResult { + // let msg = mock_end_crowdfund_msg(limit); + // self.execute(app, &msg, sender, &[]) + // } + + // pub fn execute_mint( + // &self, + // sender: Addr, + // app: &mut MockApp, + // token_id: String, + // extension: TokenExtension, + // token_uri: Option, + // owner: Option, + // ) -> ExecuteResult { + // let msg = ExecuteMsg::Mint(vec![mock_crowdfund_mint_msg( + // token_id, extension, token_uri, owner, + // )]); + // self.execute(app, &msg, sender, &[]) + // } + + // pub fn execute_quick_mint( + // &self, + // sender: Addr, + // app: &mut MockApp, + // amount: u32, + // publisher: String, + // ) -> ExecuteResult { + // let msg = mock_crowdfund_quick_mint_msg(amount, publisher); + // self.execute(app, &msg, sender, &[]) + // } + + // pub fn execute_purchase( + // &self, + // sender: Addr, + // app: &mut MockApp, + // number_of_tokens: Option, + // funds: &[Coin], + // ) -> ExecuteResult { + // let msg = mock_purchase_msg(number_of_tokens); + // self.execute(app, &msg, sender, funds) + // } } pub fn mock_andromeda_crowdfund() -> Box> { @@ -130,79 +118,77 @@ pub fn mock_andromeda_crowdfund() -> Box> { } pub fn mock_crowdfund_instantiate_msg( - token_address: AndrAddr, - can_mint_after_sale: bool, + campaign_config: CampaignConfig, modules: Option>, kernel_address: impl Into, owner: Option, ) -> InstantiateMsg { InstantiateMsg { - token_address, - can_mint_after_sale, + campaign_config, modules, kernel_address: kernel_address.into(), owner, } } -pub fn mock_start_crowdfund_msg( - start_time: Option, - end_time: Expiry, - price: Coin, - min_tokens_sold: Uint128, - max_amount_per_wallet: Option, - recipient: Recipient, -) -> ExecuteMsg { - ExecuteMsg::StartSale { - start_time, - end_time, - price, - min_tokens_sold, - max_amount_per_wallet, - recipient, - } -} - -pub fn mock_end_crowdfund_msg(limit: Option) -> ExecuteMsg { - ExecuteMsg::EndSale { limit } -} - -pub fn mock_crowdfund_mint_msg( - token_id: String, - extension: TokenExtension, - token_uri: Option, - owner: Option, -) -> CrowdfundMintMsg { - CrowdfundMintMsg { - token_id, - owner, - token_uri, - extension, - } -} - -pub fn mock_crowdfund_quick_mint_msg(amount: u32, publisher: String) -> ExecuteMsg { - let mut mint_msgs: Vec = Vec::new(); - for i in 0..amount { - let extension = TokenExtension { - publisher: publisher.clone(), - }; - - let msg = mock_crowdfund_mint_msg(i.to_string(), extension, None, None); - mint_msgs.push(msg); - } - - ExecuteMsg::Mint(mint_msgs) -} - -pub fn mock_purchase_msg(number_of_tokens: Option) -> ExecuteMsg { - ExecuteMsg::Purchase { number_of_tokens } -} - -pub fn mock_query_ado_base_version() -> QueryMsg { - QueryMsg::ADOBaseVersion {} -} - -pub fn mock_query_ado_version() -> QueryMsg { - QueryMsg::Version {} -} +// pub fn mock_start_crowdfund_msg( +// start_time: Option, +// end_time: Expiry, +// price: Coin, +// min_tokens_sold: Uint128, +// max_amount_per_wallet: Option, +// recipient: Recipient, +// ) -> ExecuteMsg { +// ExecuteMsg::StartSale { +// start_time, +// end_time, +// price, +// min_tokens_sold, +// max_amount_per_wallet, +// recipient, +// } +// } + +// pub fn mock_end_crowdfund_msg(limit: Option) -> ExecuteMsg { +// ExecuteMsg::EndSale { limit } +// } + +// pub fn mock_crowdfund_mint_msg( +// token_id: String, +// extension: TokenExtension, +// token_uri: Option, +// owner: Option, +// ) -> CrowdfundMintMsg { +// CrowdfundMintMsg { +// token_id, +// owner, +// token_uri, +// extension, +// } +// } + +// pub fn mock_crowdfund_quick_mint_msg(amount: u32, publisher: String) -> ExecuteMsg { +// let mut mint_msgs: Vec = Vec::new(); +// for i in 0..amount { +// let extension = TokenExtension { +// publisher: publisher.clone(), +// }; + +// let msg = mock_crowdfund_mint_msg(i.to_string(), extension, None, None); +// mint_msgs.push(msg); +// } + +// ExecuteMsg::Mint(mint_msgs) +// } + +// pub fn mock_purchase_msg(number_of_tokens: Option) -> ExecuteMsg { +// ExecuteMsg::Purchase { number_of_tokens } +// } + +// pub fn mock_query_ado_base_version() -> QueryMsg { +// QueryMsg::ADOBaseVersion {} +// } + +// pub fn mock_query_ado_version() -> QueryMsg { +// QueryMsg::Version {} +// } diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/state.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/state.rs index a078b2bc9..58f1f6729 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/state.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/state.rs @@ -1,53 +1,4 @@ -use andromeda_non_fungible_tokens::crowdfund::{Config, State}; -use andromeda_std::error::ContractError; -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Order, Storage, SubMsg, Uint128}; -use cw_storage_plus::{Bound, Item, Map}; +use andromeda_non_fungible_tokens::crowdfund::CampaignConfig; +use cw_storage_plus::Item; -/// The config. -pub const CONFIG: Item = Item::new("config"); - -/// The number of tokens available for sale. -pub const NUMBER_OF_TOKENS_AVAILABLE: Item = Item::new("number_of_tokens_available"); - -/// Sale started if and only if STATE.may_load is Some and !duration.is_expired() -pub const STATE: Item = Item::new("state"); - -/// Relates buyer address to vector of purchases. -pub const PURCHASES: Map<&str, Vec> = Map::new("buyers"); - -/// Contains token ids that have not been purchased. -pub const AVAILABLE_TOKENS: Map<&str, bool> = Map::new("available_tokens"); - -/// Is set to true when at least one sale has been conducted. This is used to disallow minting if -/// config.can_mint_after_sale is false. -pub const SALE_CONDUCTED: Item = Item::new("sale_conducted"); - -#[cw_serde] -pub struct Purchase { - /// The token id being purchased. - pub token_id: String, - /// Amount of tax paid. - pub tax_amount: Uint128, - /// sub messages for sending funds for rates. - pub msgs: Vec, - /// The purchaser of the token. - pub purchaser: String, -} - -const MAX_LIMIT: u32 = 50; -const DEFAULT_LIMIT: u32 = 20; -pub(crate) fn get_available_tokens( - storage: &dyn Storage, - start_after: Option, - limit: Option, -) -> Result, ContractError> { - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let start = start_after.as_deref().map(Bound::exclusive); - let tokens: Result, ContractError> = AVAILABLE_TOKENS - .keys(storage, start, None, Order::Ascending) - .take(limit) - .map(|token| Ok(token?)) - .collect(); - tokens -} +pub const CAMPAIGN_CONFIG: Item = Item::new("campaign_config"); diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs index 447c0354d..614f49f7a 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs @@ -1,35 +1,31 @@ -use andromeda_std::ado_base::hooks::{AndromedaHook, HookMsg, OnFundsTransferResponse}; -use andromeda_std::ado_base::InstantiateMsg; -use andromeda_std::ado_contract::ADOContract; -use andromeda_std::common::Funds; -use andromeda_std::testing::mock_querier::MockAndromedaQuerier; -use cosmwasm_schema::cw_serde; -use cosmwasm_std::testing::mock_info; -use cosmwasm_std::{ - coin, BankMsg, BankQuery, CosmosMsg, QuerierWrapper, Response, SubMsg, Uint128, +use andromeda_non_fungible_tokens::crowdfund::CampaignConfig; +use andromeda_std::{ + ado_base::InstantiateMsg, + ado_contract::ADOContract, + amp::AndrAddr, + testing::mock_querier::{WasmMockQuerier, MOCK_KERNEL_CONTRACT}, }; use cosmwasm_std::{ - from_json, - testing::{mock_env, MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}, - to_json_binary, Binary, Coin, ContractResult, OwnedDeps, Querier, QuerierResult, QueryRequest, - SystemError, SystemResult, WasmQuery, -}; -use cw721::{ContractInfoResponse, Cw721QueryMsg, TokensResponse}; - -pub use andromeda_std::testing::mock_querier::{ - MOCK_ADDRESS_LIST_CONTRACT, MOCK_APP_CONTRACT, MOCK_KERNEL_CONTRACT, MOCK_RATES_CONTRACT, + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + Coin, OwnedDeps, QuerierWrapper, Uint128, }; -pub const MOCK_TOKEN_CONTRACT: &str = "token_contract"; - -pub const MOCK_TAX_RECIPIENT: &str = "tax_recipient"; -pub const MOCK_ROYALTY_RECIPIENT: &str = "royalty_recipient"; -pub const MOCK_TOKENS_FOR_SALE: &[&str] = &[ - "token1", "token2", "token3", "token4", "token5", "token6", "token7", -]; - -pub const MOCK_CONDITIONS_MET_CONTRACT: &str = "conditions_met"; -pub const MOCK_CONDITIONS_NOT_MET_CONTRACT: &str = "conditions_not_met"; +pub const MOCK_TIER_CONTRACT: &str = "tier_contract"; +pub const MOCK_WITHDRAWAL_ADDRESS: &str = "withdrawal_address"; + +pub fn mock_campaign_config() -> CampaignConfig { + CampaignConfig { + title: "First Crowdfund".to_string(), + description: "Demo campaign for testing".to_string(), + banner: "http://".to_string(), + url: "http://".to_string(), + denom: "uandr".to_string(), + tier_address: AndrAddr::from_string(MOCK_TIER_CONTRACT.to_owned()), + withdrawal_address: AndrAddr::from_string(MOCK_WITHDRAWAL_ADDRESS.to_owned()), + soft_cap: None, + hard_cap: Uint128::from(5000u128), + } +} /// Alternative to `cosmwasm_std::testing::mock_dependencies` that allows us to respond to custom queries. /// @@ -38,7 +34,7 @@ pub fn mock_dependencies_custom( contract_balance: &[Coin], ) -> OwnedDeps { let custom_querier: WasmMockQuerier = - WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])); + WasmMockQuerier::new(MockQuerier::new(&[(MOCK_TIER_CONTRACT, contract_balance)])); let storage = MockStorage::default(); let mut deps = OwnedDeps { storage, @@ -64,200 +60,3 @@ pub fn mock_dependencies_custom( .unwrap(); deps } - -pub struct WasmMockQuerier { - pub base: MockQuerier, - pub contract_address: String, - pub tokens_left_to_burn: usize, -} - -impl Querier for WasmMockQuerier { - fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { - // MockQuerier doesn't support Custom, so we ignore it completely here - let request: QueryRequest = match from_json(bin_request) { - Ok(v) => v, - Err(e) => { - return SystemResult::Err(SystemError::InvalidRequest { - error: format!("Parsing query request: {e}"), - request: bin_request.into(), - }) - } - }; - self.handle_query(&request) - } -} - -// NOTE: It's impossible to construct a non_exhaustive struct from another another crate, so I copied the struct -// https://rust-lang.github.io/rfcs/2008-non-exhaustive.html#functional-record-updates -#[cw_serde( - Serialize, - Deserialize, - Clone, - Debug, - Default, - PartialEq, - Eq, - JsonSchema -)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub struct SupplyResponse { - /// Always returns a Coin with the requested denom. - /// This will be of zero amount if the denom does not exist. - pub amount: Coin, -} - -impl WasmMockQuerier { - pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { - match &request { - QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { - match contract_addr.as_str() { - MOCK_TOKEN_CONTRACT => self.handle_token_query(msg), - MOCK_RATES_CONTRACT => self.handle_rates_query(msg), - MOCK_ADDRESS_LIST_CONTRACT => self.handle_addresslist_query(msg), - _ => MockAndromedaQuerier::default().handle_query(&self.base, request), - } - } - QueryRequest::Bank(bank_query) => match bank_query { - BankQuery::Supply { denom } => { - let response = SupplyResponse { - amount: coin(1_000_000, denom), - }; - - SystemResult::Ok(ContractResult::Ok(to_json_binary(&response).unwrap())) - } - BankQuery::Balance { - address: _, - denom: _, - } => { - panic!("Unsupported Query") - } - BankQuery::AllBalances { address: _ } => { - panic!("Unsupported Query") - } - _ => panic!("Unsupported Query"), - }, - _ => MockAndromedaQuerier::default().handle_query(&self.base, request), - } - } - - fn handle_token_query(&self, msg: &Binary) -> QuerierResult { - match from_json(msg).unwrap() { - Cw721QueryMsg::Tokens { owner, .. } => { - let res = if owner == MOCK_CONDITIONS_MET_CONTRACT - || owner == MOCK_CONDITIONS_NOT_MET_CONTRACT - { - TokensResponse { - tokens: MOCK_TOKENS_FOR_SALE - [MOCK_TOKENS_FOR_SALE.len() - self.tokens_left_to_burn..] - .iter() - .copied() - .map(String::from) - .collect(), - } - } else { - TokensResponse { - tokens: MOCK_TOKENS_FOR_SALE - .iter() - .copied() - .map(String::from) - .collect(), - } - }; - - SystemResult::Ok(ContractResult::Ok(to_json_binary(&res).unwrap())) - } - Cw721QueryMsg::ContractInfo {} => { - let res = ContractInfoResponse { - name: "Test Tokens".to_string(), - symbol: "TTT".to_string(), - }; - SystemResult::Ok(ContractResult::Ok(to_json_binary(&res).unwrap())) - } - - _ => panic!("Unsupported Query"), - } - } - - fn handle_rates_query(&self, msg: &Binary) -> QuerierResult { - match from_json(msg).unwrap() { - HookMsg::AndrHook(hook_msg) => match hook_msg { - AndromedaHook::OnFundsTransfer { - sender: _, - payload: _, - amount, - } => { - let (new_funds, msgs): (Funds, Vec) = match amount { - Funds::Native(ref coin) => ( - Funds::Native(Coin { - // Deduct royalty of 10%. - amount: coin.amount.multiply_ratio(90u128, 100u128), - denom: coin.denom.clone(), - }), - vec![ - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: MOCK_ROYALTY_RECIPIENT.to_owned(), - amount: vec![Coin { - // Royalty of 10% - amount: coin.amount.multiply_ratio(10u128, 100u128), - denom: coin.denom.clone(), - }], - })), - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: MOCK_TAX_RECIPIENT.to_owned(), - amount: vec![Coin { - // Flat tax of 50 - amount: Uint128::from(50u128), - denom: coin.denom.clone(), - }], - })), - ], - ), - Funds::Cw20(_) => { - let resp: Response = Response::default(); - return SystemResult::Ok(ContractResult::Ok( - to_json_binary(&resp).unwrap(), - )); - } - }; - let response = OnFundsTransferResponse { - msgs, - events: vec![], - leftover_funds: new_funds, - }; - SystemResult::Ok(ContractResult::Ok(to_json_binary(&Some(response)).unwrap())) - } - _ => SystemResult::Ok(ContractResult::Ok( - to_json_binary(&None::).unwrap(), - )), - }, - } - } - - fn handle_addresslist_query(&self, msg: &Binary) -> QuerierResult { - match from_json(msg).unwrap() { - HookMsg::AndrHook(hook_msg) => match hook_msg { - AndromedaHook::OnExecute { sender, payload: _ } => { - let whitelisted_addresses = ["sender"]; - let response: Response = Response::default(); - if whitelisted_addresses.contains(&sender.as_str()) { - SystemResult::Ok(ContractResult::Ok(to_json_binary(&response).unwrap())) - } else { - SystemResult::Ok(ContractResult::Err("InvalidAddress".to_string())) - } - } - _ => SystemResult::Ok(ContractResult::Ok( - to_json_binary(&None::).unwrap(), - )), - }, - } - } - - pub fn new(base: MockQuerier) -> Self { - WasmMockQuerier { - base, - contract_address: mock_env().contract.address.to_string(), - tokens_left_to_burn: 2, - } - } -} diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs index 26b81388a..c7673f4ec 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs @@ -1,105 +1,22 @@ -use crate::{ - contract::{execute, instantiate, query, MAX_MINT_LIMIT}, - state::{ - Purchase, AVAILABLE_TOKENS, CONFIG, NUMBER_OF_TOKENS_AVAILABLE, PURCHASES, SALE_CONDUCTED, - STATE, - }, - testing::mock_querier::{ - mock_dependencies_custom, MOCK_ADDRESS_LIST_CONTRACT, MOCK_APP_CONTRACT, - MOCK_CONDITIONS_MET_CONTRACT, MOCK_CONDITIONS_NOT_MET_CONTRACT, MOCK_RATES_CONTRACT, - MOCK_ROYALTY_RECIPIENT, MOCK_TAX_RECIPIENT, MOCK_TOKENS_FOR_SALE, MOCK_TOKEN_CONTRACT, - }, -}; -use andromeda_non_fungible_tokens::{ - crowdfund::{ - Config, CrowdfundMintMsg, ExecuteMsg, InstantiateMsg, IsTokenAvailableResponse, QueryMsg, - State, - }, - cw721::{ExecuteMsg as Cw721ExecuteMsg, TokenExtension}, -}; -use andromeda_std::{ - ado_base::modules::Module, - amp::{addresses::AndrAddr, recipient::Recipient}, - common::{ - encode_binary, - expiration::{expiration_from_milliseconds, Expiry, MILLISECONDS_TO_NANOSECONDS_RATIO}, - Milliseconds, - }, - error::ContractError, -}; -use andromeda_testing::economics_msg::generate_economics_message; +use andromeda_non_fungible_tokens::crowdfund::InstantiateMsg; +use andromeda_std::{ado_base::Module, testing::mock_querier::MOCK_KERNEL_CONTRACT}; use cosmwasm_std::{ - coin, coins, from_json, testing::{mock_env, mock_info}, - Addr, BankMsg, Coin, CosmosMsg, DepsMut, Response, StdError, SubMsg, Uint128, WasmMsg, + DepsMut, Response, }; -use cw_utils::Expiration; - -use super::mock_querier::MOCK_KERNEL_CONTRACT; - -const ADDRESS_LIST: &str = "addresslist"; -const RATES: &str = "rates"; - -fn get_purchase(token_id: impl Into, purchaser: impl Into) -> Purchase { - Purchase { - token_id: token_id.into(), - purchaser: purchaser.into(), - tax_amount: Uint128::from(50u128), - msgs: get_rates_messages(), - } -} -fn get_rates_messages() -> Vec { - let coin = coin(100u128, "uusd"); - vec![ - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: MOCK_ROYALTY_RECIPIENT.to_owned(), - amount: vec![Coin { - // Royalty of 10% - amount: coin.amount.multiply_ratio(10u128, 100u128), - denom: coin.denom.clone(), - }], - })), - SubMsg::new(CosmosMsg::Bank(BankMsg::Send { - to_address: MOCK_TAX_RECIPIENT.to_owned(), - amount: vec![Coin { - // Flat tax of 50 - amount: Uint128::from(50u128), - denom: coin.denom, - }], - })), - ] -} - -fn get_burn_message(token_id: impl Into) -> CosmosMsg { - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: MOCK_TOKEN_CONTRACT.to_owned(), - funds: vec![], - msg: encode_binary(&Cw721ExecuteMsg::Burn { - token_id: token_id.into(), - }) - .unwrap(), - }) -} +use crate::{ + contract::instantiate, state::CAMPAIGN_CONFIG, testing::mock_querier::mock_dependencies_custom, +}; -fn get_transfer_message(token_id: impl Into, recipient: AndrAddr) -> CosmosMsg { - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: MOCK_TOKEN_CONTRACT.to_owned(), - msg: encode_binary(&Cw721ExecuteMsg::TransferNft { - recipient, - token_id: token_id.into(), - }) - .unwrap(), - funds: vec![], - }) -} +use super::mock_querier::mock_campaign_config; fn init(deps: DepsMut, modules: Option>) -> Response { + let config = mock_campaign_config(); let msg = InstantiateMsg { - token_address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), + campaign_config: config, owner: None, modules, - can_mint_after_sale: true, kernel_address: MOCK_KERNEL_CONTRACT.to_string(), }; @@ -111,1910 +28,1897 @@ fn init(deps: DepsMut, modules: Option>) -> Response { fn test_instantiate() { let mut deps = mock_dependencies_custom(&[]); - let modules = vec![Module { - name: Some(RATES.to_owned()), - address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), - is_mutable: false, - }]; - - let res = init(deps.as_mut(), Some(modules)); + let res = init(deps.as_mut(), None); assert_eq!( Response::new() .add_attribute("method", "instantiate") .add_attribute("type", "crowdfund") .add_attribute("kernel_address", MOCK_KERNEL_CONTRACT) - .add_attribute("owner", "owner") - .add_attribute("action", "register_module") - .add_attribute("module_idx", "1"), - res - ); - - assert_eq!( - Config { - token_address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), - can_mint_after_sale: true - }, - CONFIG.load(deps.as_mut().storage).unwrap() - ); - - assert!(!SALE_CONDUCTED.load(deps.as_mut().storage).unwrap()); -} - -#[test] -fn test_mint_unauthorized() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - let msg = ExecuteMsg::Mint(vec![CrowdfundMintMsg { - token_id: "token_id".to_string(), - owner: None, - token_uri: None, - extension: TokenExtension { - publisher: "publisher".to_string(), - }, - }]); - let info = mock_info("not_owner", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg); - - assert_eq!(ContractError::Unauthorized {}, res.unwrap_err()); -} - -#[test] -fn test_mint_owner_not_crowdfund() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - let msg = ExecuteMsg::Mint(vec![CrowdfundMintMsg { - token_id: "token_id".to_string(), - owner: Some("not_crowdfund".to_string()), - token_uri: None, - extension: TokenExtension { - publisher: "publisher".to_string(), - }, - }]); - let info = mock_info("owner", &[]); - let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - // Since token was minted to owner that is not the contract, it is not available for sale. - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, "token_id")); -} - -#[test] -fn test_mint_sale_started() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; - - let msg = ExecuteMsg::StartSale { - start_time: None, - end_time: Expiry::AtTime(Milliseconds::from_nanos((current_time + 2) * 1_000_000)), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: Some(5), - recipient: Recipient::from_string("recipient"), - }; - - let info = mock_info("owner", &[]); - let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - let res = mint(deps.as_mut(), "token_id"); - - assert_eq!(ContractError::SaleStarted {}, res.unwrap_err()); -} - -#[test] -fn test_mint_sale_conducted_cant_mint_after_sale() { - let mut deps = mock_dependencies_custom(&[]); - let msg = InstantiateMsg { - token_address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), - modules: None, - owner: None, - can_mint_after_sale: false, - kernel_address: MOCK_KERNEL_CONTRACT.to_string(), - }; - - let info = mock_info("owner", &[]); - let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); - - SALE_CONDUCTED.save(deps.as_mut().storage, &true).unwrap(); - - let res = mint(deps.as_mut(), "token_id"); - - assert_eq!( - ContractError::CannotMintAfterSaleConducted {}, - res.unwrap_err() - ); -} - -#[test] -fn test_mint_sale_conducted_can_mint_after_sale() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - SALE_CONDUCTED.save(deps.as_mut().storage, &true).unwrap(); - - let _res = mint(deps.as_mut(), "token_id").unwrap(); - - assert!(AVAILABLE_TOKENS.has(deps.as_ref().storage, "token_id")); -} - -#[test] -fn test_mint_successful() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - let res = mint(deps.as_mut(), "token_id").unwrap(); - - let mint_msg = Cw721ExecuteMsg::Mint { - token_id: "token_id".to_string(), - owner: mock_env().contract.address.to_string(), - token_uri: None, - extension: TokenExtension { - publisher: "publisher".to_string(), - }, - }; - - assert_eq!( - Response::new() - .add_attribute("action", "mint") - .add_message(WasmMsg::Execute { - contract_addr: MOCK_TOKEN_CONTRACT.to_owned(), - msg: encode_binary(&mint_msg).unwrap(), - funds: vec![], - }) - .add_submessage(generate_economics_message("owner", "Mint")), - res - ); - - assert!(AVAILABLE_TOKENS.has(deps.as_ref().storage, "token_id")); -} - -#[test] -fn test_mint_multiple_successful() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - let mint_msgs = vec![ - CrowdfundMintMsg { - token_id: "token_id1".to_string(), - owner: None, - token_uri: None, - extension: TokenExtension { - publisher: "publisher".to_string(), - }, - }, - CrowdfundMintMsg { - token_id: "token_id2".to_string(), - owner: None, - token_uri: None, - extension: TokenExtension { - publisher: "publisher".to_string(), - }, - }, - ]; - - let msg = ExecuteMsg::Mint(mint_msgs); - let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg).unwrap(); - - assert_eq!( - Response::new() - .add_attribute("action", "mint") - .add_attribute("action", "mint") - .add_message(WasmMsg::Execute { - contract_addr: MOCK_TOKEN_CONTRACT.to_owned(), - msg: encode_binary(&Cw721ExecuteMsg::Mint { - token_id: "token_id1".to_string(), - owner: mock_env().contract.address.to_string(), - token_uri: None, - extension: TokenExtension { - publisher: "publisher".to_string(), - }, - }) - .unwrap(), - funds: vec![], - }) - .add_message(WasmMsg::Execute { - contract_addr: MOCK_TOKEN_CONTRACT.to_owned(), - msg: encode_binary(&Cw721ExecuteMsg::Mint { - token_id: "token_id2".to_string(), - owner: mock_env().contract.address.to_string(), - token_uri: None, - extension: TokenExtension { - publisher: "publisher".to_string(), - }, - }) - .unwrap(), - funds: vec![], - }) - .add_submessage(generate_economics_message("owner", "Mint")), - res - ); - - assert!(AVAILABLE_TOKENS.has(deps.as_ref().storage, "token_id1")); - assert!(AVAILABLE_TOKENS.has(deps.as_ref().storage, "token_id2")); - - assert_eq!( - NUMBER_OF_TOKENS_AVAILABLE - .load(deps.as_ref().storage) - .unwrap(), - Uint128::new(2) - ); -} - -#[test] -fn test_mint_multiple_exceeds_limit() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - let mint_msg = CrowdfundMintMsg { - token_id: "token_id1".to_string(), - owner: None, - token_uri: None, - extension: TokenExtension { - publisher: "publisher".to_string(), - }, - }; - - let mut mint_msgs: Vec = vec![]; - - for _ in 0..MAX_MINT_LIMIT + 1 { - mint_msgs.push(mint_msg.clone()); - } - - let msg = ExecuteMsg::Mint(mint_msgs.clone()); - let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); - - assert_eq!( - ContractError::TooManyMintMessages { - limit: MAX_MINT_LIMIT - }, - res.unwrap_err() - ); -} - -#[test] -fn test_start_sale_end_time_zero() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - let one_minute_in_future = - mock_env().block.time.plus_minutes(1).nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; - - let msg = ExecuteMsg::StartSale { - start_time: Some(Expiry::AtTime(Milliseconds(one_minute_in_future))), - end_time: Expiry::AtTime(Milliseconds::zero()), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: None, - recipient: Recipient::from_string("recipient".to_string()), - }; - - let info = mock_info("owner", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(ContractError::StartTimeAfterEndTime {}, res.unwrap_err()); -} - -#[test] -fn test_start_sale_unauthorized() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; - - let msg = ExecuteMsg::StartSale { - start_time: None, - end_time: Expiry::AtTime(Milliseconds::from_nanos((current_time + 1) * 1_000_000)), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: None, - recipient: Recipient::from_string("recipient"), - }; - - let info = mock_info("anyone", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(ContractError::Unauthorized {}, res.unwrap_err()); -} - -#[test] -fn test_start_sale_start_time_in_past() { - let mut deps = mock_dependencies_custom(&[]); - let env = mock_env(); - init(deps.as_mut(), None); - let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; - - let one_minute_in_past = env.block.time.minus_minutes(1).seconds(); - let msg = ExecuteMsg::StartSale { - start_time: Some(Expiry::AtTime(Milliseconds(one_minute_in_past))), - end_time: Expiry::AtTime(Milliseconds::from_nanos((current_time + 2) * 1_000_000)), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: None, - recipient: Recipient::from_string("recipient"), - }; - - let info = mock_info("owner", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!( - ContractError::StartTimeInThePast { - current_time: env.block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO, - current_block: env.block.height, - }, - res.unwrap_err() - ); -} - -#[test] -fn test_start_sale_start_time_in_future() { - let mut deps = mock_dependencies_custom(&[]); - let env = mock_env(); - init(deps.as_mut(), None); - - let one_minute_in_future = - env.block.time.plus_minutes(1).nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; - let msg = ExecuteMsg::StartSale { - start_time: Some(Expiry::AtTime(Milliseconds(one_minute_in_future))), - end_time: Expiry::AtTime(Milliseconds::from_nanos( - (one_minute_in_future + 2) * 1_000_000, - )), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: None, - recipient: Recipient::from_string("recipient"), - }; - - let info = mock_info("owner", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert!(res.is_ok()) -} - -#[test] -fn test_start_sale_max_default() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; - - let msg = ExecuteMsg::StartSale { - start_time: None, - end_time: Expiry::AtTime(Milliseconds::from_nanos((current_time + 2) * 1_000_000)), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: None, - recipient: Recipient::from_string("recipient"), - }; - - let info = mock_info("owner", &[]); - let res = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap(); - // Using current time since start time wasn't provided - let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; - let start_expiration = expiration_from_milliseconds(Milliseconds(current_time + 1)).unwrap(); - let end_expiration = expiration_from_milliseconds(Milliseconds(current_time + 2)).unwrap(); - - assert_eq!( - Response::new() - .add_attribute("action", "start_sale") - .add_attribute("start_time", start_expiration.to_string()) - .add_attribute("end_time", end_expiration.to_string()) - .add_attribute("price", "100uusd") - .add_attribute("min_tokens_sold", "1") - .add_attribute("max_amount_per_wallet", "1") - .add_submessage(generate_economics_message("owner", "StartSale")), - res - ); - - assert_eq!( - State { - end_time: end_expiration, - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 1, - amount_sold: Uint128::zero(), - amount_to_send: Uint128::zero(), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }, - STATE.load(deps.as_ref().storage).unwrap() - ); - - assert!(SALE_CONDUCTED.load(deps.as_ref().storage).unwrap()); - - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(ContractError::SaleStarted {}, res.unwrap_err()); -} - -#[test] -fn test_start_sale_max_modified() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; - - let msg = ExecuteMsg::StartSale { - start_time: None, - end_time: Expiry::AtTime(Milliseconds::from_nanos((current_time + 2) * 1_000_000)), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: Some(5), - recipient: Recipient::from_string("recipient"), - }; - // Using current time since start time wasn't provided - let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; - let start_expiration = expiration_from_milliseconds(Milliseconds(current_time + 1)).unwrap(); - let end_expiration = expiration_from_milliseconds(Milliseconds(current_time + 2)).unwrap(); - - let info = mock_info("owner", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - assert_eq!( - Response::new() - .add_attribute("action", "start_sale") - .add_attribute("start_time", start_expiration.to_string()) - .add_attribute("end_time", end_expiration.to_string()) - .add_attribute("price", "100uusd") - .add_attribute("min_tokens_sold", "1") - .add_attribute("max_amount_per_wallet", "5") - .add_submessage(generate_economics_message("owner", "StartSale")), - res - ); - - assert_eq!( - State { - end_time: end_expiration, - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 5, - amount_sold: Uint128::zero(), - amount_to_send: Uint128::zero(), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }, - STATE.load(deps.as_ref().storage).unwrap() - ); -} - -#[test] -fn test_purchase_sale_not_started() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - let msg = ExecuteMsg::Purchase { - number_of_tokens: None, - }; - - let info = mock_info("sender", &[]); - let res = execute(deps.as_mut(), mock_env(), info.clone(), msg); - assert_eq!(ContractError::NoOngoingSale {}, res.unwrap_err()); - - let msg = ExecuteMsg::PurchaseByTokenId { - token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), - }; - - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(ContractError::NoOngoingSale {}, res.unwrap_err()); -} - -#[test] -fn test_purchase_sale_not_ended() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - STATE - .save( - deps.as_mut().storage, - &State { - end_time: Expiration::AtHeight(mock_env().block.height - 1), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 5, - amount_sold: Uint128::zero(), - amount_to_send: Uint128::zero(), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }, - ) - .unwrap(); - - let info = mock_info("sender", &[]); - - let msg = ExecuteMsg::Purchase { - number_of_tokens: None, - }; - - let res = execute(deps.as_mut(), mock_env(), info.clone(), msg); - assert_eq!(ContractError::NoOngoingSale {}, res.unwrap_err()); - - let msg = ExecuteMsg::PurchaseByTokenId { - token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), - }; - - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(ContractError::NoOngoingSale {}, res.unwrap_err()); -} - -#[test] -fn test_purchase_no_funds() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); - - STATE - .save( - deps.as_mut().storage, - &State { - end_time: Expiration::AtHeight(mock_env().block.height + 1), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 5, - amount_sold: Uint128::zero(), - amount_to_send: Uint128::zero(), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }, - ) - .unwrap(); - - let info = mock_info("sender", &[]); - - let msg = ExecuteMsg::Purchase { - number_of_tokens: None, - }; - let res = execute(deps.as_mut(), mock_env(), info.clone(), msg); - assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); - - let msg = ExecuteMsg::PurchaseByTokenId { - token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), - }; - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); -} - -#[test] -fn test_purchase_wrong_denom() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); - - STATE - .save( - deps.as_mut().storage, - &State { - end_time: Expiration::AtHeight(mock_env().block.height + 1), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 5, - amount_sold: Uint128::zero(), - amount_to_send: Uint128::zero(), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }, - ) - .unwrap(); - - let info = mock_info("sender", &coins(100, "uluna")); - - let msg = ExecuteMsg::Purchase { - number_of_tokens: None, - }; - let res = execute(deps.as_mut(), mock_env(), info.clone(), msg); - assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); - - let msg = ExecuteMsg::PurchaseByTokenId { - token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), - }; - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); -} - -#[test] -fn test_purchase_not_enough_for_price() { - let mut deps = mock_dependencies_custom(&[]); - let modules = vec![Module { - name: Some(RATES.to_owned()), - address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), - is_mutable: false, - }]; - init(deps.as_mut(), Some(modules)); - - mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); - - STATE - .save( - deps.as_mut().storage, - &State { - end_time: Expiration::AtHeight(mock_env().block.height + 1), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 5, - amount_sold: Uint128::zero(), - amount_to_send: Uint128::zero(), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }, - ) - .unwrap(); - - let info = mock_info("sender", &coins(50u128, "uusd")); - - let msg = ExecuteMsg::Purchase { - number_of_tokens: None, - }; - let res = execute(deps.as_mut(), mock_env(), info.clone(), msg); - assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); - - let msg = ExecuteMsg::PurchaseByTokenId { - token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), - }; - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); -} - -#[test] -fn test_purchase_not_enough_for_tax() { - let mut deps = mock_dependencies_custom(&[]); - let modules = vec![Module { - name: Some(RATES.to_owned()), - address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), - is_mutable: false, - }]; - init(deps.as_mut(), Some(modules)); - - mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); - - NUMBER_OF_TOKENS_AVAILABLE - .save(deps.as_mut().storage, &Uint128::new(1)) - .unwrap(); - - STATE - .save( - deps.as_mut().storage, - &State { - end_time: Expiration::AtHeight(mock_env().block.height + 1), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 5, - amount_sold: Uint128::zero(), - amount_to_send: Uint128::zero(), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }, - ) - .unwrap(); - - let info = mock_info("sender", &coins(100u128, "uusd")); - - let msg = ExecuteMsg::Purchase { - number_of_tokens: None, - }; - let res = execute(deps.as_mut(), mock_env(), info.clone(), msg); - assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); - - // Reset the state since state does not roll back on failure in tests like it does in prod. - AVAILABLE_TOKENS - .save(deps.as_mut().storage, MOCK_TOKENS_FOR_SALE[0], &true) - .unwrap(); - NUMBER_OF_TOKENS_AVAILABLE - .save(deps.as_mut().storage, &Uint128::new(1)) - .unwrap(); - - let msg = ExecuteMsg::PurchaseByTokenId { - token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), - }; - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); -} - -#[test] -fn test_purchase_by_token_id_not_available() { - let mut deps = mock_dependencies_custom(&[]); - let modules = vec![Module { - name: Some(RATES.to_owned()), - address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), - is_mutable: false, - }]; - init(deps.as_mut(), Some(modules)); - - mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); - - STATE - .save( - deps.as_mut().storage, - &State { - end_time: Expiration::AtHeight(mock_env().block.height + 1), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 5, - amount_sold: Uint128::zero(), - amount_to_send: Uint128::zero(), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }, - ) - .unwrap(); - - let info = mock_info("sender", &coins(150, "uusd")); - - let msg = ExecuteMsg::PurchaseByTokenId { - token_id: MOCK_TOKENS_FOR_SALE[1].to_owned(), - }; - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(ContractError::TokenNotAvailable {}, res.unwrap_err()); -} - -#[test] -fn test_purchase_by_token_id() { - let mut deps = mock_dependencies_custom(&[]); - let modules = vec![Module { - name: Some(RATES.to_owned()), - address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), - is_mutable: false, - }]; - init(deps.as_mut(), Some(modules)); - - mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); - mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[1]).unwrap(); - - let mut state = State { - end_time: Expiration::AtHeight(mock_env().block.height + 1), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 1, - amount_sold: Uint128::zero(), - amount_to_send: Uint128::zero(), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }; - - STATE.save(deps.as_mut().storage, &state).unwrap(); - - let info = mock_info("sender", &coins(150, "uusd")); - - // Purchase a token. - let msg = ExecuteMsg::PurchaseByTokenId { - token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), - }; - let res = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - assert_eq!( - Response::new() - .add_attribute("action", "purchase") - .add_attribute("token_id", MOCK_TOKENS_FOR_SALE[0]) - .add_submessage(generate_economics_message("sender", "PurchaseByTokenId")), + .add_attribute("owner", "owner"), res ); - state.amount_to_send += Uint128::from(90u128); - state.amount_sold += Uint128::from(1u128); - assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); - - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[0])); assert_eq!( - NUMBER_OF_TOKENS_AVAILABLE - .load(deps.as_ref().storage) - .unwrap(), - Uint128::new(1) + mock_campaign_config(), + CAMPAIGN_CONFIG.load(deps.as_mut().storage).unwrap() ); - - // Purchase a second one. - let msg = ExecuteMsg::PurchaseByTokenId { - token_id: MOCK_TOKENS_FOR_SALE[1].to_owned(), - }; - let res = execute(deps.as_mut(), mock_env(), info, msg); - - assert_eq!(ContractError::PurchaseLimitReached {}, res.unwrap_err()); } -#[test] -fn test_multiple_purchases() { - let mut deps = mock_dependencies_custom(&[]); - let modules = vec![Module { - name: Some(RATES.to_owned()), - address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), - is_mutable: false, - }]; - init(deps.as_mut(), Some(modules)); - - // Mint four tokens. - mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); - mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[1]).unwrap(); - mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[2]).unwrap(); - mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[3]).unwrap(); - - // Query available tokens. - let msg = QueryMsg::AvailableTokens { - start_after: None, - limit: None, - }; - let res: Vec = from_json(query(deps.as_ref(), mock_env(), msg).unwrap()).unwrap(); - assert_eq!( - vec![ - MOCK_TOKENS_FOR_SALE[0], - MOCK_TOKENS_FOR_SALE[1], - MOCK_TOKENS_FOR_SALE[2], - MOCK_TOKENS_FOR_SALE[3] - ], - res - ); - - // Query if individual token is available - let msg = QueryMsg::IsTokenAvailable { - id: MOCK_TOKENS_FOR_SALE[0].to_owned(), - }; - let res: IsTokenAvailableResponse = - from_json(query(deps.as_ref(), mock_env(), msg).unwrap()).unwrap(); - assert!(res.is_token_available); - - // Query if another token is available - let msg = QueryMsg::IsTokenAvailable { - id: MOCK_TOKENS_FOR_SALE[4].to_owned(), - }; - let res: IsTokenAvailableResponse = - from_json(query(deps.as_ref(), mock_env(), msg).unwrap()).unwrap(); - assert!(!res.is_token_available); - - // Purchase 2 tokens - let msg = ExecuteMsg::Purchase { - number_of_tokens: Some(2), - }; - - let mut state = State { - end_time: Expiration::AtHeight(mock_env().block.height + 1), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 3, - amount_sold: Uint128::zero(), - amount_to_send: Uint128::zero(), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }; - STATE.save(deps.as_mut().storage, &state).unwrap(); - - let info = mock_info("sender", &coins(300u128, "uusd")); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - assert_eq!( - Response::new() - .add_attribute("action", "purchase") - .add_attribute("number_of_tokens_wanted", "2") - .add_attribute("number_of_tokens_purchased", "2") - .add_submessage(generate_economics_message("sender", "Purchase")), - res - ); - - state.amount_to_send += Uint128::from(180u128); - state.amount_sold += Uint128::from(2u128); - assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); - - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[0])); - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[1])); - - assert_eq!( - vec![ - get_purchase(MOCK_TOKENS_FOR_SALE[0], "sender"), - get_purchase(MOCK_TOKENS_FOR_SALE[1], "sender") - ], - PURCHASES.load(deps.as_ref().storage, "sender").unwrap() - ); - - // Purchase max number of tokens. - let msg = ExecuteMsg::Purchase { - number_of_tokens: None, - }; - - let info = mock_info("sender", &coins(300u128, "uusd")); - let res = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - - assert_eq!( - Response::new() - .add_message(BankMsg::Send { - to_address: "sender".to_string(), - // Refund sent back as they only were able to mint one. - amount: coins(150, "uusd") - }) - .add_attribute("action", "purchase") - .add_attribute("number_of_tokens_wanted", "1") - .add_attribute("number_of_tokens_purchased", "1") - .add_submessage(generate_economics_message("sender", "Purchase")), - res - ); - - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[2])); - state.amount_to_send += Uint128::from(90u128); - state.amount_sold += Uint128::from(1u128); - assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); - - assert_eq!( - vec![ - get_purchase(MOCK_TOKENS_FOR_SALE[0], "sender"), - get_purchase(MOCK_TOKENS_FOR_SALE[1], "sender"), - get_purchase(MOCK_TOKENS_FOR_SALE[2], "sender") - ], - PURCHASES.load(deps.as_ref().storage, "sender").unwrap() - ); - - // Try to purchase an additional token when limit has already been reached. - let msg = ExecuteMsg::Purchase { - number_of_tokens: Some(1), - }; - - let res = execute(deps.as_mut(), mock_env(), info, msg); - - assert_eq!(ContractError::PurchaseLimitReached {}, res.unwrap_err()); - - // User 2 tries to purchase 2 but only 1 is left. - let msg = ExecuteMsg::Purchase { - number_of_tokens: Some(2), - }; - - let info = mock_info("user2", &coins(300, "uusd")); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - assert_eq!( - Response::new() - .add_message(BankMsg::Send { - to_address: "user2".to_string(), - // Refund sent back as they only were able to mint one. - amount: coins(150, "uusd") - }) - .add_attribute("action", "purchase") - .add_attribute("number_of_tokens_wanted", "2") - .add_attribute("number_of_tokens_purchased", "1") - .add_submessage(generate_economics_message("user2", "Purchase")), - res - ); - - assert_eq!( - vec![get_purchase(MOCK_TOKENS_FOR_SALE[3], "user2"),], - PURCHASES.load(deps.as_ref().storage, "user2").unwrap() - ); - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[3])); - state.amount_to_send += Uint128::from(90u128); - state.amount_sold += Uint128::from(1u128); - assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); - - assert_eq!( - NUMBER_OF_TOKENS_AVAILABLE - .load(deps.as_ref().storage) - .unwrap(), - Uint128::zero() - ); - - // User 2 tries to purchase again. - let msg = ExecuteMsg::Purchase { - number_of_tokens: None, - }; - - let info = mock_info("user2", &coins(150, "uusd")); - let res = execute(deps.as_mut(), mock_env(), info, msg); - - assert_eq!(ContractError::AllTokensPurchased {}, res.unwrap_err()); -} - -#[test] -fn test_purchase_more_than_allowed_per_wallet() { - let mut deps = mock_dependencies_custom(&[]); - let modules = vec![Module { - name: Some(RATES.to_owned()), - address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), - is_mutable: false, - }]; - init(deps.as_mut(), Some(modules)); - - // Mint four tokens. - mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); - mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[1]).unwrap(); - mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[2]).unwrap(); - mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[3]).unwrap(); - - // Try to purchase 4 - let msg = ExecuteMsg::Purchase { - number_of_tokens: Some(4), - }; - - let state = State { - end_time: Expiration::AtHeight(mock_env().block.height + 1), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 3, - amount_sold: Uint128::zero(), - amount_to_send: Uint128::zero(), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }; - STATE.save(deps.as_mut().storage, &state).unwrap(); - - let info = mock_info("sender", &coins(600, "uusd")); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - assert_eq!( - Response::new() - .add_message(BankMsg::Send { - to_address: "sender".to_string(), - amount: coins(150, "uusd") - }) - .add_attribute("action", "purchase") - // Number got truncated to 3 which is the max possible. - .add_attribute("number_of_tokens_wanted", "3") - .add_attribute("number_of_tokens_purchased", "3") - .add_submessage(generate_economics_message("sender", "Purchase")), - res - ); -} - -#[test] -fn test_end_sale_not_expired() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - let state = State { - end_time: Expiration::AtHeight(mock_env().block.height + 1), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 2, - amount_sold: Uint128::zero(), - amount_to_send: Uint128::zero(), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }; - STATE.save(deps.as_mut().storage, &state).unwrap(); - NUMBER_OF_TOKENS_AVAILABLE - .save(deps.as_mut().storage, &Uint128::new(1)) - .unwrap(); - - let msg = ExecuteMsg::EndSale { limit: None }; - let info = mock_info("anyone", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(ContractError::SaleNotEnded {}, res.unwrap_err()); -} - -fn mint(deps: DepsMut, token_id: impl Into) -> Result { - let msg = ExecuteMsg::Mint(vec![CrowdfundMintMsg { - token_id: token_id.into(), - owner: None, - token_uri: None, - extension: TokenExtension { - publisher: "publisher".to_string(), - }, - }]); - execute(deps, mock_env(), mock_info("owner", &[]), msg) -} - -#[test] -fn test_integration_conditions_not_met() { - let mut deps = mock_dependencies_custom(&[]); - let modules = vec![Module { - name: Some(RATES.to_owned()), - address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), - is_mutable: false, - }]; - init(deps.as_mut(), Some(modules)); - - // Mint all tokens. - for &token_id in MOCK_TOKENS_FOR_SALE { - let _res = mint(deps.as_mut(), token_id).unwrap(); - assert!(AVAILABLE_TOKENS.has(deps.as_ref().storage, token_id)); - } - - assert_eq!( - NUMBER_OF_TOKENS_AVAILABLE - .load(deps.as_ref().storage) - .unwrap(), - Uint128::new(7) - ); - let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; - - let msg = ExecuteMsg::StartSale { - start_time: None, - end_time: Expiry::AtTime(Milliseconds::from_nanos((current_time + 2) * 1_000_000)), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(5u128), - max_amount_per_wallet: Some(2), - recipient: Recipient::from_string("recipient"), - }; - - let info = mock_info("owner", &[]); - let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - // Can't mint once sale started. - let res = mint(deps.as_mut(), "token_id"); - assert_eq!(ContractError::SaleStarted {}, res.unwrap_err()); - - let msg = ExecuteMsg::Purchase { - number_of_tokens: Some(1), - }; - let info = mock_info("A", &coins(150, "uusd")); - let _res = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - - let msg = ExecuteMsg::Purchase { - number_of_tokens: Some(1), - }; - let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - let msg = ExecuteMsg::Purchase { - number_of_tokens: Some(1), - }; - let info = mock_info("B", &coins(150, "uusd")); - let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - let msg = ExecuteMsg::Purchase { - number_of_tokens: Some(1), - }; - let info = mock_info("C", &coins(150, "uusd")); - let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - // Using current time since start time wasn't provided - let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; - let end_expiration = expiration_from_milliseconds(Milliseconds(current_time + 2)).unwrap(); - - let state = State { - end_time: end_expiration, - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(5u128), - max_amount_per_wallet: 2, - amount_sold: Uint128::from(4u128), - amount_to_send: Uint128::from(360u128), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }; - assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); - - assert_eq!( - vec![ - get_purchase(MOCK_TOKENS_FOR_SALE[0], "A"), - get_purchase(MOCK_TOKENS_FOR_SALE[1], "A") - ], - PURCHASES.load(deps.as_ref().storage, "A").unwrap() - ); - - assert_eq!( - vec![get_purchase(MOCK_TOKENS_FOR_SALE[2], "B"),], - PURCHASES.load(deps.as_ref().storage, "B").unwrap() - ); - - assert_eq!( - vec![get_purchase(MOCK_TOKENS_FOR_SALE[3], "C"),], - PURCHASES.load(deps.as_ref().storage, "C").unwrap() - ); - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[0])); - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[1])); - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[2])); - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[3])); - - assert_eq!( - NUMBER_OF_TOKENS_AVAILABLE - .load(deps.as_ref().storage) - .unwrap(), - Uint128::new(3) - ); - - let mut env = mock_env(); - env.block.time = env.block.time.plus_hours(1); - - // User B claims their own refund. - let msg = ExecuteMsg::ClaimRefund {}; - let info = mock_info("B", &[]); - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - assert_eq!( - Response::new() - .add_attribute("action", "claim_refund") - .add_message(CosmosMsg::Bank(BankMsg::Send { - to_address: "B".to_string(), - amount: coins(150, "uusd"), - })) - .add_submessage(generate_economics_message("B", "ClaimRefund")), - res - ); - - assert!(!PURCHASES.has(deps.as_ref().storage, "B")); - - env.contract.address = Addr::unchecked(MOCK_CONDITIONS_NOT_MET_CONTRACT); - deps.querier.tokens_left_to_burn = 7; - let msg = ExecuteMsg::EndSale { limit: None }; - let info = mock_info("anyone", &[]); - let res = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()).unwrap(); - let refund_msgs: Vec = vec![ - // All of A's payments grouped into one message. - CosmosMsg::Bank(BankMsg::Send { - to_address: "A".to_string(), - amount: coins(300, "uusd"), - }), - CosmosMsg::Bank(BankMsg::Send { - to_address: "C".to_string(), - amount: coins(150, "uusd"), - }), - ]; - let burn_msgs: Vec = vec![ - get_burn_message(MOCK_TOKENS_FOR_SALE[0]), - get_burn_message(MOCK_TOKENS_FOR_SALE[1]), - get_burn_message(MOCK_TOKENS_FOR_SALE[2]), - get_burn_message(MOCK_TOKENS_FOR_SALE[3]), - // Tokens that were not sold. - get_burn_message(MOCK_TOKENS_FOR_SALE[4]), - get_burn_message(MOCK_TOKENS_FOR_SALE[5]), - get_burn_message(MOCK_TOKENS_FOR_SALE[6]), - ]; - - assert_eq!( - Response::new() - .add_attribute("action", "issue_refunds_and_burn_tokens") - .add_messages(refund_msgs) - .add_messages(burn_msgs) - .add_submessage(generate_economics_message("anyone", "EndSale")), - res - ); - - assert!(!PURCHASES.has(deps.as_ref().storage, "A")); - assert!(!PURCHASES.has(deps.as_ref().storage, "C")); - - // Burned tokens have been removed. - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[4])); - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[5])); - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[6])); - - deps.querier.tokens_left_to_burn = 0; - let _res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert!(STATE.may_load(deps.as_mut().storage).unwrap().is_none()); - assert_eq!( - NUMBER_OF_TOKENS_AVAILABLE - .load(deps.as_ref().storage) - .unwrap(), - Uint128::zero() - ); -} - -#[test] -fn test_integration_conditions_met() { - let mut deps = mock_dependencies_custom(&[]); - deps.querier.contract_address = MOCK_CONDITIONS_MET_CONTRACT.to_string(); - let modules = vec![Module { - name: Some(RATES.to_owned()), - address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), - is_mutable: false, - }]; - init(deps.as_mut(), Some(modules)); - let mut env = mock_env(); - env.contract.address = Addr::unchecked(MOCK_CONDITIONS_MET_CONTRACT); - - // Mint all tokens. - for &token_id in MOCK_TOKENS_FOR_SALE { - let _res = mint(deps.as_mut(), token_id).unwrap(); - assert!(AVAILABLE_TOKENS.has(deps.as_ref().storage, token_id)); - } - let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; - - let msg = ExecuteMsg::StartSale { - start_time: None, - end_time: Expiry::AtTime(Milliseconds::from_nanos((current_time + 2) * 1_000_000)), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(3u128), - max_amount_per_wallet: Some(2), - recipient: Recipient::from_string("recipient"), - }; - - let info = mock_info("owner", &[]); - let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - - let msg = ExecuteMsg::Purchase { - number_of_tokens: Some(1), - }; - let info = mock_info("A", &coins(150, "uusd")); - let _res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - - let msg = ExecuteMsg::Purchase { - number_of_tokens: Some(1), - }; - let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - - let msg = ExecuteMsg::Purchase { - number_of_tokens: Some(1), - }; - let info = mock_info("B", &coins(150, "uusd")); - let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - - let msg = ExecuteMsg::Purchase { - number_of_tokens: Some(1), - }; - let info = mock_info("C", &coins(150, "uusd")); - let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - - let msg = ExecuteMsg::Purchase { - number_of_tokens: Some(1), - }; - let info = mock_info("D", &coins(150, "uusd")); - let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); - // Using current time since start time wasn't provided - let current_time = env.block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; - let end_expiration = expiration_from_milliseconds(Milliseconds(current_time + 2)).unwrap(); - let mut state = State { - end_time: end_expiration, - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(3u128), - max_amount_per_wallet: 2, - amount_sold: Uint128::from(5u128), - amount_to_send: Uint128::from(450u128), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }; - assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); - - assert_eq!( - vec![ - get_purchase(MOCK_TOKENS_FOR_SALE[0], "A"), - get_purchase(MOCK_TOKENS_FOR_SALE[1], "A") - ], - PURCHASES.load(deps.as_ref().storage, "A").unwrap() - ); - - assert_eq!( - vec![get_purchase(MOCK_TOKENS_FOR_SALE[2], "B"),], - PURCHASES.load(deps.as_ref().storage, "B").unwrap() - ); - assert_eq!( - vec![get_purchase(MOCK_TOKENS_FOR_SALE[3], "C"),], - PURCHASES.load(deps.as_ref().storage, "C").unwrap() - ); - assert_eq!( - vec![get_purchase(MOCK_TOKENS_FOR_SALE[4], "D"),], - PURCHASES.load(deps.as_ref().storage, "D").unwrap() - ); - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[0])); - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[1])); - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[2])); - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[3])); - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[4])); - - env.block.time = env.block.time.plus_hours(1); - env.contract.address = Addr::unchecked(MOCK_CONDITIONS_MET_CONTRACT); - - let msg = ExecuteMsg::EndSale { limit: Some(1) }; - let info = mock_info("anyone", &[]); - let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - - assert_eq!( - Response::new() - .add_attribute("action", "transfer_tokens_and_send_funds") - .add_message(get_transfer_message( - MOCK_TOKENS_FOR_SALE[0], - AndrAddr::from_string("A") - )) - .add_submessages(get_rates_messages()) - .add_submessage(generate_economics_message("anyone", "EndSale")), - res - ); - - assert_eq!( - vec![get_purchase(MOCK_TOKENS_FOR_SALE[1], "A")], - PURCHASES.load(deps.as_ref().storage, "A").unwrap() - ); - - state.amount_transferred += Uint128::from(1u128); - assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); - - let msg = ExecuteMsg::EndSale { limit: Some(2) }; - let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - - assert_eq!( - Response::new() - .add_attribute("action", "transfer_tokens_and_send_funds") - .add_message(get_transfer_message( - MOCK_TOKENS_FOR_SALE[1], - AndrAddr::from_string("A") - )) - .add_message(get_transfer_message( - MOCK_TOKENS_FOR_SALE[2], - AndrAddr::from_string("B") - )) - .add_message(CosmosMsg::Bank(BankMsg::Send { - to_address: MOCK_ROYALTY_RECIPIENT.to_owned(), - amount: vec![Coin { - // Royalty of 10% for A and B combined - amount: Uint128::from(20u128), - denom: "uusd".to_string(), - }], - })) - .add_message(CosmosMsg::Bank(BankMsg::Send { - to_address: MOCK_TAX_RECIPIENT.to_owned(), - amount: vec![Coin { - // Combined tax for both A and B - amount: Uint128::from(100u128), - denom: "uusd".to_string(), - }], - })) - .add_submessage(generate_economics_message("anyone", "EndSale")), - res - ); - - assert!(!PURCHASES.has(deps.as_ref().storage, "A"),); - assert!(!PURCHASES.has(deps.as_ref().storage, "B"),); - assert!(PURCHASES.has(deps.as_ref().storage, "C"),); - assert!(PURCHASES.has(deps.as_ref().storage, "D"),); - - state.amount_transferred += Uint128::from(2u128); - assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); - - let msg = ExecuteMsg::EndSale { limit: None }; - let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - - assert!(!PURCHASES.has(deps.as_ref().storage, "C"),); - assert!(!PURCHASES.has(deps.as_ref().storage, "D"),); - - assert_eq!( - Response::new() - .add_attribute("action", "transfer_tokens_and_send_funds") - .add_message(get_transfer_message( - MOCK_TOKENS_FOR_SALE[3], - AndrAddr::from_string("C") - )) - .add_message(get_transfer_message( - MOCK_TOKENS_FOR_SALE[4], - AndrAddr::from_string("D") - )) - .add_message(CosmosMsg::Bank(BankMsg::Send { - to_address: MOCK_ROYALTY_RECIPIENT.to_owned(), - amount: vec![Coin { - // Royalty of 10% for C and D combined - amount: Uint128::from(20u128), - denom: "uusd".to_string(), - }], - })) - .add_message(CosmosMsg::Bank(BankMsg::Send { - to_address: MOCK_TAX_RECIPIENT.to_owned(), - amount: vec![Coin { - // Combined tax for both C and D - amount: Uint128::from(100u128), - denom: "uusd".to_string(), - }], - })) - .add_submessage(generate_economics_message("anyone", "EndSale")), - res - ); - - state.amount_transferred += Uint128::from(2u128); - assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); - - let msg = ExecuteMsg::EndSale { limit: None }; - let res = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()).unwrap(); - // Added one for economics message - assert_eq!(3 + 1, res.messages.len()); - - // assert_eq!( - // Response::new() - // .add_attribute("action", "transfer_tokens_and_send_funds") - // // Now that all tokens have been transfered, can send the funds to recipient. - // .add_message(CosmosMsg::Bank(BankMsg::Send { - // to_address: "recipient".to_string(), - // amount: coins(450u128, "uusd") - // })) - // // Burn tokens that were not purchased - // .add_message(get_burn_message(MOCK_TOKENS_FOR_SALE[5])) - // .add_message(get_burn_message(MOCK_TOKENS_FOR_SALE[6])), - // res - // ); - - state.amount_to_send = Uint128::zero(); - assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); - - // Burned tokens removed. - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[5])); - assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[6])); - - deps.querier.tokens_left_to_burn = 0; - let _res = execute(deps.as_mut(), env, info, msg).unwrap(); - assert!(STATE.may_load(deps.as_mut().storage).unwrap().is_none()); - assert_eq!( - NUMBER_OF_TOKENS_AVAILABLE - .load(deps.as_ref().storage) - .unwrap(), - Uint128::zero() - ); -} - -#[test] -fn test_end_sale_single_purchase() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - STATE - .save( - deps.as_mut().storage, - &State { - end_time: Expiration::AtHeight(mock_env().block.height - 1), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 5, - amount_sold: Uint128::from(1u128), - amount_to_send: Uint128::from(100u128), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }, - ) - .unwrap(); - - PURCHASES - .save( - deps.as_mut().storage, - "A", - &vec![Purchase { - token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), - purchaser: "A".to_string(), - tax_amount: Uint128::zero(), - msgs: vec![], - }], - ) - .unwrap(); - - let msg = ExecuteMsg::EndSale { limit: None }; - let info = mock_info("anyone", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - assert_eq!( - Response::new() - .add_attribute("action", "transfer_tokens_and_send_funds") - // Burn tokens that were not purchased - .add_message(get_transfer_message( - MOCK_TOKENS_FOR_SALE[0], - AndrAddr::from_string("A") - )) - .add_submessage(generate_economics_message("anyone", "EndSale")), - res - ); -} - -#[test] -fn test_end_sale_all_tokens_sold() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - STATE - .save( - deps.as_mut().storage, - &State { - // Sale has not expired yet. - end_time: Expiration::AtHeight(mock_env().block.height + 1), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 5, - amount_sold: Uint128::from(1u128), - amount_to_send: Uint128::from(100u128), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }, - ) - .unwrap(); - - PURCHASES - .save( - deps.as_mut().storage, - "A", - &vec![Purchase { - token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), - purchaser: "A".to_string(), - tax_amount: Uint128::zero(), - msgs: vec![], - }], - ) - .unwrap(); - - NUMBER_OF_TOKENS_AVAILABLE - .save(deps.as_mut().storage, &Uint128::zero()) - .unwrap(); - - let msg = ExecuteMsg::EndSale { limit: None }; - let info = mock_info("anyone", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - assert_eq!( - Response::new() - .add_attribute("action", "transfer_tokens_and_send_funds") - // Burn tokens that were not purchased - .add_message(get_transfer_message( - MOCK_TOKENS_FOR_SALE[0], - AndrAddr::from_string("A") - )) - .add_submessage(generate_economics_message("anyone", "EndSale")), - res - ); -} - -#[test] -fn test_end_sale_some_tokens_sold_threshold_met() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - STATE - .save( - deps.as_mut().storage, - &State { - // Sale has not expired yet. - end_time: Expiration::AtHeight(mock_env().block.height + 1), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 5, - amount_sold: Uint128::from(2u128), - amount_to_send: Uint128::from(100u128), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }, - ) - .unwrap(); - - PURCHASES - .save( - deps.as_mut().storage, - "A", - &vec![Purchase { - token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), - purchaser: "A".to_string(), - tax_amount: Uint128::zero(), - msgs: vec![], - }], - ) - .unwrap(); - - NUMBER_OF_TOKENS_AVAILABLE - .save(deps.as_mut().storage, &Uint128::one()) - .unwrap(); - - let msg = ExecuteMsg::EndSale { limit: None }; - // Only the owner can end the sale if only the minimum token threshold is met. - // Anyone can end the sale if it's expired or the remaining number of tokens available is zero. - let info = mock_info("anyone", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap_err(); - assert_eq!(err, ContractError::SaleNotEnded {}); - - let info = mock_info("owner", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - assert_eq!( - Response::new() - .add_attribute("action", "transfer_tokens_and_send_funds") - // Burn tokens that were not purchased - .add_message(get_transfer_message( - MOCK_TOKENS_FOR_SALE[0], - AndrAddr::from_string("A") - )) - .add_submessage(generate_economics_message("owner", "EndSale")), - res - ); -} - -#[test] -fn test_end_sale_some_tokens_sold_threshold_not_met() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - STATE - .save( - deps.as_mut().storage, - &State { - // Sale has not expired yet. - end_time: Expiration::AtHeight(mock_env().block.height + 1), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(2u128), - max_amount_per_wallet: 5, - amount_sold: Uint128::from(0u128), - amount_to_send: Uint128::from(100u128), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }, - ) - .unwrap(); - - PURCHASES - .save( - deps.as_mut().storage, - "A", - &vec![Purchase { - token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), - purchaser: "A".to_string(), - tax_amount: Uint128::zero(), - msgs: vec![], - }], - ) - .unwrap(); - - NUMBER_OF_TOKENS_AVAILABLE - .save(deps.as_mut().storage, &Uint128::new(2)) - .unwrap(); - - let msg = ExecuteMsg::EndSale { limit: None }; - - let info = mock_info("owner", &[]); - // Minimum sold is 2, actual sold is 0 - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(err, ContractError::SaleNotEnded {}); -} - -#[test] -fn test_end_sale_limit_zero() { - let mut deps = mock_dependencies_custom(&[]); - init(deps.as_mut(), None); - - STATE - .save( - deps.as_mut().storage, - &State { - end_time: Expiration::AtHeight(mock_env().block.height - 1), - price: coin(100, "uusd"), - min_tokens_sold: Uint128::from(1u128), - max_amount_per_wallet: 5, - amount_sold: Uint128::from(1u128), - amount_to_send: Uint128::from(100u128), - amount_transferred: Uint128::zero(), - recipient: Recipient::from_string("recipient"), - }, - ) - .unwrap(); - NUMBER_OF_TOKENS_AVAILABLE - .save(deps.as_mut().storage, &Uint128::new(1)) - .unwrap(); - - PURCHASES - .save( - deps.as_mut().storage, - "A", - &vec![Purchase { - token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), - purchaser: "A".to_string(), - tax_amount: Uint128::zero(), - msgs: vec![], - }], - ) - .unwrap(); - - let msg = ExecuteMsg::EndSale { limit: Some(0) }; - let info = mock_info("anyone", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg); - - assert_eq!(ContractError::LimitMustNotBeZero {}, res.unwrap_err()); -} - -#[test] -fn test_validate_andr_addresses_regular_address() { - let mut deps = mock_dependencies_custom(&[]); - let msg = InstantiateMsg { - token_address: AndrAddr::from_string("terra1asdf1ssdfadf".to_owned()), - owner: None, - modules: None, - can_mint_after_sale: true, - kernel_address: MOCK_KERNEL_CONTRACT.to_string(), - }; - - let info = mock_info("owner", &[]); - let _res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - - let msg = ExecuteMsg::UpdateAppContract { - address: MOCK_APP_CONTRACT.to_owned(), - }; - - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - assert_eq!( - Response::new() - .add_attribute("action", "update_app_contract") - .add_attribute("address", MOCK_APP_CONTRACT) - .add_submessage(generate_economics_message("owner", "UpdateAppContract")), - res - ); -} - -#[test] -fn test_addresslist() { - let mut deps = mock_dependencies_custom(&[]); - let modules = vec![Module { - name: Some(ADDRESS_LIST.to_owned()), - address: AndrAddr::from_string(MOCK_ADDRESS_LIST_CONTRACT.to_owned()), - is_mutable: false, - }]; - let msg = InstantiateMsg { - token_address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), - modules: Some(modules), - can_mint_after_sale: true, - owner: None, - kernel_address: MOCK_KERNEL_CONTRACT.to_string(), - }; - - let info = mock_info("owner", &[]); - let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); - - // Not whitelisted user - let msg = ExecuteMsg::Purchase { - number_of_tokens: None, - }; - let info = mock_info("not_whitelisted", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg); - - assert_eq!( - ContractError::Std(StdError::generic_err( - "Querier contract error: InvalidAddress" - )), - res.unwrap_err() - ); -} - -#[test] -fn test_update_token_contract() { - let mut deps = mock_dependencies_custom(&[]); - let msg = InstantiateMsg { - token_address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), - modules: None, - can_mint_after_sale: true, - owner: None, - kernel_address: MOCK_KERNEL_CONTRACT.to_string(), - }; - - let info = mock_info("owner", &[]); - let _res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - - let msg = ExecuteMsg::UpdateTokenContract { - address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), - }; - - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert!(res.is_ok()) -} - -#[test] -fn test_update_token_contract_unauthorized() { - let mut deps = mock_dependencies_custom(&[]); - let msg = InstantiateMsg { - token_address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), - modules: None, - can_mint_after_sale: true, - owner: None, - kernel_address: MOCK_KERNEL_CONTRACT.to_string(), - }; - - let info = mock_info("app_contract", &[]); - let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); - - let msg = ExecuteMsg::UpdateTokenContract { - address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), - }; - - let unauth_info = mock_info("attacker", &[]); - let res = execute(deps.as_mut(), mock_env(), unauth_info, msg).unwrap_err(); - assert_eq!(ContractError::Unauthorized {}, res); -} - -#[test] -fn test_update_token_contract_post_mint() { - let mut deps = mock_dependencies_custom(&[]); - let msg = InstantiateMsg { - token_address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), - modules: None, - can_mint_after_sale: true, - owner: None, - kernel_address: MOCK_KERNEL_CONTRACT.to_string(), - }; - - let info = mock_info("owner", &[]); - let _res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - - mint(deps.as_mut(), "1").unwrap(); - - let msg = ExecuteMsg::UpdateTokenContract { - address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), - }; - - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(ContractError::Unauthorized {}, res); -} - -#[test] -fn test_update_token_contract_not_cw721() { - let mut deps = mock_dependencies_custom(&[]); - let msg = InstantiateMsg { - token_address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), - modules: None, - can_mint_after_sale: true, - owner: None, - kernel_address: MOCK_KERNEL_CONTRACT.to_string(), - }; - - let info = mock_info("owner", &[]); - let _res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - - let msg = ExecuteMsg::UpdateTokenContract { - address: AndrAddr::from_string("not_a_token_contract".to_owned()), - }; - - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(ContractError::Unauthorized {}, res); -} +// #[test] +// fn test_mint_unauthorized() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// let msg = ExecuteMsg::Mint(vec![CrowdfundMintMsg { +// token_id: "token_id".to_string(), +// owner: None, +// token_uri: None, +// extension: TokenExtension { +// publisher: "publisher".to_string(), +// }, +// }]); +// let info = mock_info("not_owner", &[]); +// let res = execute(deps.as_mut(), mock_env(), info, msg); + +// assert_eq!(ContractError::Unauthorized {}, res.unwrap_err()); +// } + +// #[test] +// fn test_mint_owner_not_crowdfund() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// let msg = ExecuteMsg::Mint(vec![CrowdfundMintMsg { +// token_id: "token_id".to_string(), +// owner: Some("not_crowdfund".to_string()), +// token_uri: None, +// extension: TokenExtension { +// publisher: "publisher".to_string(), +// }, +// }]); +// let info = mock_info("owner", &[]); +// let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// // Since token was minted to owner that is not the contract, it is not available for sale. +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, "token_id")); +// } + +// #[test] +// fn test_mint_sale_started() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); +// let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; + +// let msg = ExecuteMsg::StartSale { +// start_time: None, +// end_time: Expiry::AtTime(Milliseconds::from_nanos((current_time + 2) * 1_000_000)), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: Some(5), +// recipient: Recipient::from_string("recipient"), +// }; + +// let info = mock_info("owner", &[]); +// let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// let res = mint(deps.as_mut(), "token_id"); + +// assert_eq!(ContractError::SaleStarted {}, res.unwrap_err()); +// } + +// #[test] +// fn test_mint_sale_conducted_cant_mint_after_sale() { +// let mut deps = mock_dependencies_custom(&[]); +// let msg = InstantiateMsg { +// token_address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), +// modules: None, +// owner: None, +// can_mint_after_sale: false, +// kernel_address: MOCK_KERNEL_CONTRACT.to_string(), +// }; + +// let info = mock_info("owner", &[]); +// let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// SALE_CONDUCTED.save(deps.as_mut().storage, &true).unwrap(); + +// let res = mint(deps.as_mut(), "token_id"); + +// assert_eq!( +// ContractError::CannotMintAfterSaleConducted {}, +// res.unwrap_err() +// ); +// } + +// #[test] +// fn test_mint_sale_conducted_can_mint_after_sale() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// SALE_CONDUCTED.save(deps.as_mut().storage, &true).unwrap(); + +// let _res = mint(deps.as_mut(), "token_id").unwrap(); + +// assert!(AVAILABLE_TOKENS.has(deps.as_ref().storage, "token_id")); +// } + +// #[test] +// fn test_mint_successful() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// let res = mint(deps.as_mut(), "token_id").unwrap(); + +// let mint_msg = Cw721ExecuteMsg::Mint { +// token_id: "token_id".to_string(), +// owner: mock_env().contract.address.to_string(), +// token_uri: None, +// extension: TokenExtension { +// publisher: "publisher".to_string(), +// }, +// }; + +// assert_eq!( +// Response::new() +// .add_attribute("action", "mint") +// .add_message(WasmMsg::Execute { +// contract_addr: MOCK_TOKEN_CONTRACT.to_owned(), +// msg: encode_binary(&mint_msg).unwrap(), +// funds: vec![], +// }) +// .add_submessage(generate_economics_message("owner", "Mint")), +// res +// ); + +// assert!(AVAILABLE_TOKENS.has(deps.as_ref().storage, "token_id")); +// } + +// #[test] +// fn test_mint_multiple_successful() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// let mint_msgs = vec![ +// CrowdfundMintMsg { +// token_id: "token_id1".to_string(), +// owner: None, +// token_uri: None, +// extension: TokenExtension { +// publisher: "publisher".to_string(), +// }, +// }, +// CrowdfundMintMsg { +// token_id: "token_id2".to_string(), +// owner: None, +// token_uri: None, +// extension: TokenExtension { +// publisher: "publisher".to_string(), +// }, +// }, +// ]; + +// let msg = ExecuteMsg::Mint(mint_msgs); +// let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg).unwrap(); + +// assert_eq!( +// Response::new() +// .add_attribute("action", "mint") +// .add_attribute("action", "mint") +// .add_message(WasmMsg::Execute { +// contract_addr: MOCK_TOKEN_CONTRACT.to_owned(), +// msg: encode_binary(&Cw721ExecuteMsg::Mint { +// token_id: "token_id1".to_string(), +// owner: mock_env().contract.address.to_string(), +// token_uri: None, +// extension: TokenExtension { +// publisher: "publisher".to_string(), +// }, +// }) +// .unwrap(), +// funds: vec![], +// }) +// .add_message(WasmMsg::Execute { +// contract_addr: MOCK_TOKEN_CONTRACT.to_owned(), +// msg: encode_binary(&Cw721ExecuteMsg::Mint { +// token_id: "token_id2".to_string(), +// owner: mock_env().contract.address.to_string(), +// token_uri: None, +// extension: TokenExtension { +// publisher: "publisher".to_string(), +// }, +// }) +// .unwrap(), +// funds: vec![], +// }) +// .add_submessage(generate_economics_message("owner", "Mint")), +// res +// ); + +// assert!(AVAILABLE_TOKENS.has(deps.as_ref().storage, "token_id1")); +// assert!(AVAILABLE_TOKENS.has(deps.as_ref().storage, "token_id2")); + +// assert_eq!( +// NUMBER_OF_TOKENS_AVAILABLE +// .load(deps.as_ref().storage) +// .unwrap(), +// Uint128::new(2) +// ); +// } + +// #[test] +// fn test_mint_multiple_exceeds_limit() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// let mint_msg = CrowdfundMintMsg { +// token_id: "token_id1".to_string(), +// owner: None, +// token_uri: None, +// extension: TokenExtension { +// publisher: "publisher".to_string(), +// }, +// }; + +// let mut mint_msgs: Vec = vec![]; + +// for _ in 0..MAX_MINT_LIMIT + 1 { +// mint_msgs.push(mint_msg.clone()); +// } + +// let msg = ExecuteMsg::Mint(mint_msgs.clone()); +// let res = execute(deps.as_mut(), mock_env(), mock_info("owner", &[]), msg); + +// assert_eq!( +// ContractError::TooManyMintMessages { +// limit: MAX_MINT_LIMIT +// }, +// res.unwrap_err() +// ); +// } + +// #[test] +// fn test_start_sale_end_time_zero() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); +// let one_minute_in_future = +// mock_env().block.time.plus_minutes(1).nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; + +// let msg = ExecuteMsg::StartSale { +// start_time: Some(Expiry::AtTime(Milliseconds(one_minute_in_future))), +// end_time: Expiry::AtTime(Milliseconds::zero()), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: None, +// recipient: Recipient::from_string("recipient".to_string()), +// }; + +// let info = mock_info("owner", &[]); +// let res = execute(deps.as_mut(), mock_env(), info, msg); +// assert_eq!(ContractError::StartTimeAfterEndTime {}, res.unwrap_err()); +// } + +// #[test] +// fn test_start_sale_unauthorized() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); +// let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; + +// let msg = ExecuteMsg::StartSale { +// start_time: None, +// end_time: Expiry::AtTime(Milliseconds::from_nanos((current_time + 1) * 1_000_000)), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: None, +// recipient: Recipient::from_string("recipient"), +// }; + +// let info = mock_info("anyone", &[]); +// let res = execute(deps.as_mut(), mock_env(), info, msg); +// assert_eq!(ContractError::Unauthorized {}, res.unwrap_err()); +// } + +// #[test] +// fn test_start_sale_start_time_in_past() { +// let mut deps = mock_dependencies_custom(&[]); +// let env = mock_env(); +// init(deps.as_mut(), None); +// let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; + +// let one_minute_in_past = env.block.time.minus_minutes(1).seconds(); +// let msg = ExecuteMsg::StartSale { +// start_time: Some(Expiry::AtTime(Milliseconds(one_minute_in_past))), +// end_time: Expiry::AtTime(Milliseconds::from_nanos((current_time + 2) * 1_000_000)), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: None, +// recipient: Recipient::from_string("recipient"), +// }; + +// let info = mock_info("owner", &[]); +// let res = execute(deps.as_mut(), mock_env(), info, msg); +// assert_eq!( +// ContractError::StartTimeInThePast { +// current_time: env.block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO, +// current_block: env.block.height, +// }, +// res.unwrap_err() +// ); +// } + +// #[test] +// fn test_start_sale_start_time_in_future() { +// let mut deps = mock_dependencies_custom(&[]); +// let env = mock_env(); +// init(deps.as_mut(), None); + +// let one_minute_in_future = +// env.block.time.plus_minutes(1).nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; +// let msg = ExecuteMsg::StartSale { +// start_time: Some(Expiry::AtTime(Milliseconds(one_minute_in_future))), +// end_time: Expiry::AtTime(Milliseconds::from_nanos( +// (one_minute_in_future + 2) * 1_000_000, +// )), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: None, +// recipient: Recipient::from_string("recipient"), +// }; + +// let info = mock_info("owner", &[]); +// let res = execute(deps.as_mut(), mock_env(), info, msg); +// assert!(res.is_ok()) +// } + +// #[test] +// fn test_start_sale_max_default() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); +// let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; + +// let msg = ExecuteMsg::StartSale { +// start_time: None, +// end_time: Expiry::AtTime(Milliseconds::from_nanos((current_time + 2) * 1_000_000)), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: None, +// recipient: Recipient::from_string("recipient"), +// }; + +// let info = mock_info("owner", &[]); +// let res = execute(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap(); +// // Using current time since start time wasn't provided +// let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; +// let start_expiration = expiration_from_milliseconds(Milliseconds(current_time + 1)).unwrap(); +// let end_expiration = expiration_from_milliseconds(Milliseconds(current_time + 2)).unwrap(); + +// assert_eq!( +// Response::new() +// .add_attribute("action", "start_sale") +// .add_attribute("start_time", start_expiration.to_string()) +// .add_attribute("end_time", end_expiration.to_string()) +// .add_attribute("price", "100uusd") +// .add_attribute("min_tokens_sold", "1") +// .add_attribute("max_amount_per_wallet", "1") +// .add_submessage(generate_economics_message("owner", "StartSale")), +// res +// ); + +// assert_eq!( +// State { +// end_time: end_expiration, +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 1, +// amount_sold: Uint128::zero(), +// amount_to_send: Uint128::zero(), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }, +// STATE.load(deps.as_ref().storage).unwrap() +// ); + +// assert!(SALE_CONDUCTED.load(deps.as_ref().storage).unwrap()); + +// let res = execute(deps.as_mut(), mock_env(), info, msg); +// assert_eq!(ContractError::SaleStarted {}, res.unwrap_err()); +// } + +// #[test] +// fn test_start_sale_max_modified() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); +// let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; + +// let msg = ExecuteMsg::StartSale { +// start_time: None, +// end_time: Expiry::AtTime(Milliseconds::from_nanos((current_time + 2) * 1_000_000)), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: Some(5), +// recipient: Recipient::from_string("recipient"), +// }; +// // Using current time since start time wasn't provided +// let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; +// let start_expiration = expiration_from_milliseconds(Milliseconds(current_time + 1)).unwrap(); +// let end_expiration = expiration_from_milliseconds(Milliseconds(current_time + 2)).unwrap(); + +// let info = mock_info("owner", &[]); +// let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); +// assert_eq!( +// Response::new() +// .add_attribute("action", "start_sale") +// .add_attribute("start_time", start_expiration.to_string()) +// .add_attribute("end_time", end_expiration.to_string()) +// .add_attribute("price", "100uusd") +// .add_attribute("min_tokens_sold", "1") +// .add_attribute("max_amount_per_wallet", "5") +// .add_submessage(generate_economics_message("owner", "StartSale")), +// res +// ); + +// assert_eq!( +// State { +// end_time: end_expiration, +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 5, +// amount_sold: Uint128::zero(), +// amount_to_send: Uint128::zero(), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }, +// STATE.load(deps.as_ref().storage).unwrap() +// ); +// } + +// #[test] +// fn test_purchase_sale_not_started() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: None, +// }; + +// let info = mock_info("sender", &[]); +// let res = execute(deps.as_mut(), mock_env(), info.clone(), msg); +// assert_eq!(ContractError::NoOngoingSale {}, res.unwrap_err()); + +// let msg = ExecuteMsg::PurchaseByTokenId { +// token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), +// }; + +// let res = execute(deps.as_mut(), mock_env(), info, msg); +// assert_eq!(ContractError::NoOngoingSale {}, res.unwrap_err()); +// } + +// #[test] +// fn test_purchase_sale_not_ended() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// STATE +// .save( +// deps.as_mut().storage, +// &State { +// end_time: Expiration::AtHeight(mock_env().block.height - 1), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 5, +// amount_sold: Uint128::zero(), +// amount_to_send: Uint128::zero(), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }, +// ) +// .unwrap(); + +// let info = mock_info("sender", &[]); + +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: None, +// }; + +// let res = execute(deps.as_mut(), mock_env(), info.clone(), msg); +// assert_eq!(ContractError::NoOngoingSale {}, res.unwrap_err()); + +// let msg = ExecuteMsg::PurchaseByTokenId { +// token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), +// }; + +// let res = execute(deps.as_mut(), mock_env(), info, msg); +// assert_eq!(ContractError::NoOngoingSale {}, res.unwrap_err()); +// } + +// #[test] +// fn test_purchase_no_funds() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); + +// STATE +// .save( +// deps.as_mut().storage, +// &State { +// end_time: Expiration::AtHeight(mock_env().block.height + 1), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 5, +// amount_sold: Uint128::zero(), +// amount_to_send: Uint128::zero(), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }, +// ) +// .unwrap(); + +// let info = mock_info("sender", &[]); + +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: None, +// }; +// let res = execute(deps.as_mut(), mock_env(), info.clone(), msg); +// assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); + +// let msg = ExecuteMsg::PurchaseByTokenId { +// token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), +// }; +// let res = execute(deps.as_mut(), mock_env(), info, msg); +// assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); +// } + +// #[test] +// fn test_purchase_wrong_denom() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); + +// STATE +// .save( +// deps.as_mut().storage, +// &State { +// end_time: Expiration::AtHeight(mock_env().block.height + 1), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 5, +// amount_sold: Uint128::zero(), +// amount_to_send: Uint128::zero(), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }, +// ) +// .unwrap(); + +// let info = mock_info("sender", &coins(100, "uluna")); + +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: None, +// }; +// let res = execute(deps.as_mut(), mock_env(), info.clone(), msg); +// assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); + +// let msg = ExecuteMsg::PurchaseByTokenId { +// token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), +// }; +// let res = execute(deps.as_mut(), mock_env(), info, msg); +// assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); +// } + +// #[test] +// fn test_purchase_not_enough_for_price() { +// let mut deps = mock_dependencies_custom(&[]); +// let modules = vec![Module { +// name: Some(RATES.to_owned()), +// address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), +// is_mutable: false, +// }]; +// init(deps.as_mut(), Some(modules)); + +// mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); + +// STATE +// .save( +// deps.as_mut().storage, +// &State { +// end_time: Expiration::AtHeight(mock_env().block.height + 1), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 5, +// amount_sold: Uint128::zero(), +// amount_to_send: Uint128::zero(), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }, +// ) +// .unwrap(); + +// let info = mock_info("sender", &coins(50u128, "uusd")); + +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: None, +// }; +// let res = execute(deps.as_mut(), mock_env(), info.clone(), msg); +// assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); + +// let msg = ExecuteMsg::PurchaseByTokenId { +// token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), +// }; +// let res = execute(deps.as_mut(), mock_env(), info, msg); +// assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); +// } + +// #[test] +// fn test_purchase_not_enough_for_tax() { +// let mut deps = mock_dependencies_custom(&[]); +// let modules = vec![Module { +// name: Some(RATES.to_owned()), +// address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), +// is_mutable: false, +// }]; +// init(deps.as_mut(), Some(modules)); + +// mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); + +// NUMBER_OF_TOKENS_AVAILABLE +// .save(deps.as_mut().storage, &Uint128::new(1)) +// .unwrap(); + +// STATE +// .save( +// deps.as_mut().storage, +// &State { +// end_time: Expiration::AtHeight(mock_env().block.height + 1), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 5, +// amount_sold: Uint128::zero(), +// amount_to_send: Uint128::zero(), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }, +// ) +// .unwrap(); + +// let info = mock_info("sender", &coins(100u128, "uusd")); + +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: None, +// }; +// let res = execute(deps.as_mut(), mock_env(), info.clone(), msg); +// assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); + +// // Reset the state since state does not roll back on failure in tests like it does in prod. +// AVAILABLE_TOKENS +// .save(deps.as_mut().storage, MOCK_TOKENS_FOR_SALE[0], &true) +// .unwrap(); +// NUMBER_OF_TOKENS_AVAILABLE +// .save(deps.as_mut().storage, &Uint128::new(1)) +// .unwrap(); + +// let msg = ExecuteMsg::PurchaseByTokenId { +// token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), +// }; +// let res = execute(deps.as_mut(), mock_env(), info, msg); +// assert_eq!(ContractError::InsufficientFunds {}, res.unwrap_err()); +// } + +// #[test] +// fn test_purchase_by_token_id_not_available() { +// let mut deps = mock_dependencies_custom(&[]); +// let modules = vec![Module { +// name: Some(RATES.to_owned()), +// address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), +// is_mutable: false, +// }]; +// init(deps.as_mut(), Some(modules)); + +// mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); + +// STATE +// .save( +// deps.as_mut().storage, +// &State { +// end_time: Expiration::AtHeight(mock_env().block.height + 1), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 5, +// amount_sold: Uint128::zero(), +// amount_to_send: Uint128::zero(), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }, +// ) +// .unwrap(); + +// let info = mock_info("sender", &coins(150, "uusd")); + +// let msg = ExecuteMsg::PurchaseByTokenId { +// token_id: MOCK_TOKENS_FOR_SALE[1].to_owned(), +// }; +// let res = execute(deps.as_mut(), mock_env(), info, msg); +// assert_eq!(ContractError::TokenNotAvailable {}, res.unwrap_err()); +// } + +// #[test] +// fn test_purchase_by_token_id() { +// let mut deps = mock_dependencies_custom(&[]); +// let modules = vec![Module { +// name: Some(RATES.to_owned()), +// address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), +// is_mutable: false, +// }]; +// init(deps.as_mut(), Some(modules)); + +// mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); +// mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[1]).unwrap(); + +// let mut state = State { +// end_time: Expiration::AtHeight(mock_env().block.height + 1), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 1, +// amount_sold: Uint128::zero(), +// amount_to_send: Uint128::zero(), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }; + +// STATE.save(deps.as_mut().storage, &state).unwrap(); + +// let info = mock_info("sender", &coins(150, "uusd")); + +// // Purchase a token. +// let msg = ExecuteMsg::PurchaseByTokenId { +// token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), +// }; +// let res = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); +// assert_eq!( +// Response::new() +// .add_attribute("action", "purchase") +// .add_attribute("token_id", MOCK_TOKENS_FOR_SALE[0]) +// .add_submessage(generate_economics_message("sender", "PurchaseByTokenId")), +// res +// ); + +// state.amount_to_send += Uint128::from(90u128); +// state.amount_sold += Uint128::from(1u128); +// assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); + +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[0])); +// assert_eq!( +// NUMBER_OF_TOKENS_AVAILABLE +// .load(deps.as_ref().storage) +// .unwrap(), +// Uint128::new(1) +// ); + +// // Purchase a second one. +// let msg = ExecuteMsg::PurchaseByTokenId { +// token_id: MOCK_TOKENS_FOR_SALE[1].to_owned(), +// }; +// let res = execute(deps.as_mut(), mock_env(), info, msg); + +// assert_eq!(ContractError::PurchaseLimitReached {}, res.unwrap_err()); +// } + +// #[test] +// fn test_multiple_purchases() { +// let mut deps = mock_dependencies_custom(&[]); +// let modules = vec![Module { +// name: Some(RATES.to_owned()), +// address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), +// is_mutable: false, +// }]; +// init(deps.as_mut(), Some(modules)); + +// // Mint four tokens. +// mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); +// mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[1]).unwrap(); +// mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[2]).unwrap(); +// mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[3]).unwrap(); + +// // Query available tokens. +// let msg = QueryMsg::AvailableTokens { +// start_after: None, +// limit: None, +// }; +// let res: Vec = from_json(query(deps.as_ref(), mock_env(), msg).unwrap()).unwrap(); +// assert_eq!( +// vec![ +// MOCK_TOKENS_FOR_SALE[0], +// MOCK_TOKENS_FOR_SALE[1], +// MOCK_TOKENS_FOR_SALE[2], +// MOCK_TOKENS_FOR_SALE[3] +// ], +// res +// ); + +// // Query if individual token is available +// let msg = QueryMsg::IsTokenAvailable { +// id: MOCK_TOKENS_FOR_SALE[0].to_owned(), +// }; +// let res: IsTokenAvailableResponse = +// from_json(query(deps.as_ref(), mock_env(), msg).unwrap()).unwrap(); +// assert!(res.is_token_available); + +// // Query if another token is available +// let msg = QueryMsg::IsTokenAvailable { +// id: MOCK_TOKENS_FOR_SALE[4].to_owned(), +// }; +// let res: IsTokenAvailableResponse = +// from_json(query(deps.as_ref(), mock_env(), msg).unwrap()).unwrap(); +// assert!(!res.is_token_available); + +// // Purchase 2 tokens +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: Some(2), +// }; + +// let mut state = State { +// end_time: Expiration::AtHeight(mock_env().block.height + 1), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 3, +// amount_sold: Uint128::zero(), +// amount_to_send: Uint128::zero(), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }; +// STATE.save(deps.as_mut().storage, &state).unwrap(); + +// let info = mock_info("sender", &coins(300u128, "uusd")); +// let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// assert_eq!( +// Response::new() +// .add_attribute("action", "purchase") +// .add_attribute("number_of_tokens_wanted", "2") +// .add_attribute("number_of_tokens_purchased", "2") +// .add_submessage(generate_economics_message("sender", "Purchase")), +// res +// ); + +// state.amount_to_send += Uint128::from(180u128); +// state.amount_sold += Uint128::from(2u128); +// assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); + +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[0])); +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[1])); + +// assert_eq!( +// vec![ +// get_purchase(MOCK_TOKENS_FOR_SALE[0], "sender"), +// get_purchase(MOCK_TOKENS_FOR_SALE[1], "sender") +// ], +// PURCHASES.load(deps.as_ref().storage, "sender").unwrap() +// ); + +// // Purchase max number of tokens. +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: None, +// }; + +// let info = mock_info("sender", &coins(300u128, "uusd")); +// let res = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + +// assert_eq!( +// Response::new() +// .add_message(BankMsg::Send { +// to_address: "sender".to_string(), +// // Refund sent back as they only were able to mint one. +// amount: coins(150, "uusd") +// }) +// .add_attribute("action", "purchase") +// .add_attribute("number_of_tokens_wanted", "1") +// .add_attribute("number_of_tokens_purchased", "1") +// .add_submessage(generate_economics_message("sender", "Purchase")), +// res +// ); + +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[2])); +// state.amount_to_send += Uint128::from(90u128); +// state.amount_sold += Uint128::from(1u128); +// assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); + +// assert_eq!( +// vec![ +// get_purchase(MOCK_TOKENS_FOR_SALE[0], "sender"), +// get_purchase(MOCK_TOKENS_FOR_SALE[1], "sender"), +// get_purchase(MOCK_TOKENS_FOR_SALE[2], "sender") +// ], +// PURCHASES.load(deps.as_ref().storage, "sender").unwrap() +// ); + +// // Try to purchase an additional token when limit has already been reached. +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: Some(1), +// }; + +// let res = execute(deps.as_mut(), mock_env(), info, msg); + +// assert_eq!(ContractError::PurchaseLimitReached {}, res.unwrap_err()); + +// // User 2 tries to purchase 2 but only 1 is left. +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: Some(2), +// }; + +// let info = mock_info("user2", &coins(300, "uusd")); +// let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// assert_eq!( +// Response::new() +// .add_message(BankMsg::Send { +// to_address: "user2".to_string(), +// // Refund sent back as they only were able to mint one. +// amount: coins(150, "uusd") +// }) +// .add_attribute("action", "purchase") +// .add_attribute("number_of_tokens_wanted", "2") +// .add_attribute("number_of_tokens_purchased", "1") +// .add_submessage(generate_economics_message("user2", "Purchase")), +// res +// ); + +// assert_eq!( +// vec![get_purchase(MOCK_TOKENS_FOR_SALE[3], "user2"),], +// PURCHASES.load(deps.as_ref().storage, "user2").unwrap() +// ); +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[3])); +// state.amount_to_send += Uint128::from(90u128); +// state.amount_sold += Uint128::from(1u128); +// assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); + +// assert_eq!( +// NUMBER_OF_TOKENS_AVAILABLE +// .load(deps.as_ref().storage) +// .unwrap(), +// Uint128::zero() +// ); + +// // User 2 tries to purchase again. +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: None, +// }; + +// let info = mock_info("user2", &coins(150, "uusd")); +// let res = execute(deps.as_mut(), mock_env(), info, msg); + +// assert_eq!(ContractError::AllTokensPurchased {}, res.unwrap_err()); +// } + +// #[test] +// fn test_purchase_more_than_allowed_per_wallet() { +// let mut deps = mock_dependencies_custom(&[]); +// let modules = vec![Module { +// name: Some(RATES.to_owned()), +// address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), +// is_mutable: false, +// }]; +// init(deps.as_mut(), Some(modules)); + +// // Mint four tokens. +// mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[0]).unwrap(); +// mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[1]).unwrap(); +// mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[2]).unwrap(); +// mint(deps.as_mut(), MOCK_TOKENS_FOR_SALE[3]).unwrap(); + +// // Try to purchase 4 +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: Some(4), +// }; + +// let state = State { +// end_time: Expiration::AtHeight(mock_env().block.height + 1), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 3, +// amount_sold: Uint128::zero(), +// amount_to_send: Uint128::zero(), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }; +// STATE.save(deps.as_mut().storage, &state).unwrap(); + +// let info = mock_info("sender", &coins(600, "uusd")); +// let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// assert_eq!( +// Response::new() +// .add_message(BankMsg::Send { +// to_address: "sender".to_string(), +// amount: coins(150, "uusd") +// }) +// .add_attribute("action", "purchase") +// // Number got truncated to 3 which is the max possible. +// .add_attribute("number_of_tokens_wanted", "3") +// .add_attribute("number_of_tokens_purchased", "3") +// .add_submessage(generate_economics_message("sender", "Purchase")), +// res +// ); +// } + +// #[test] +// fn test_end_sale_not_expired() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// let state = State { +// end_time: Expiration::AtHeight(mock_env().block.height + 1), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 2, +// amount_sold: Uint128::zero(), +// amount_to_send: Uint128::zero(), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }; +// STATE.save(deps.as_mut().storage, &state).unwrap(); +// NUMBER_OF_TOKENS_AVAILABLE +// .save(deps.as_mut().storage, &Uint128::new(1)) +// .unwrap(); + +// let msg = ExecuteMsg::EndSale { limit: None }; +// let info = mock_info("anyone", &[]); +// let res = execute(deps.as_mut(), mock_env(), info, msg); +// assert_eq!(ContractError::SaleNotEnded {}, res.unwrap_err()); +// } + +// fn mint(deps: DepsMut, token_id: impl Into) -> Result { +// let msg = ExecuteMsg::Mint(vec![CrowdfundMintMsg { +// token_id: token_id.into(), +// owner: None, +// token_uri: None, +// extension: TokenExtension { +// publisher: "publisher".to_string(), +// }, +// }]); +// execute(deps, mock_env(), mock_info("owner", &[]), msg) +// } + +// #[test] +// fn test_integration_conditions_not_met() { +// let mut deps = mock_dependencies_custom(&[]); +// let modules = vec![Module { +// name: Some(RATES.to_owned()), +// address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), +// is_mutable: false, +// }]; +// init(deps.as_mut(), Some(modules)); + +// // Mint all tokens. +// for &token_id in MOCK_TOKENS_FOR_SALE { +// let _res = mint(deps.as_mut(), token_id).unwrap(); +// assert!(AVAILABLE_TOKENS.has(deps.as_ref().storage, token_id)); +// } + +// assert_eq!( +// NUMBER_OF_TOKENS_AVAILABLE +// .load(deps.as_ref().storage) +// .unwrap(), +// Uint128::new(7) +// ); +// let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; + +// let msg = ExecuteMsg::StartSale { +// start_time: None, +// end_time: Expiry::AtTime(Milliseconds::from_nanos((current_time + 2) * 1_000_000)), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(5u128), +// max_amount_per_wallet: Some(2), +// recipient: Recipient::from_string("recipient"), +// }; + +// let info = mock_info("owner", &[]); +// let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// // Can't mint once sale started. +// let res = mint(deps.as_mut(), "token_id"); +// assert_eq!(ContractError::SaleStarted {}, res.unwrap_err()); + +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: Some(1), +// }; +// let info = mock_info("A", &coins(150, "uusd")); +// let _res = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: Some(1), +// }; +// let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: Some(1), +// }; +// let info = mock_info("B", &coins(150, "uusd")); +// let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: Some(1), +// }; +// let info = mock_info("C", &coins(150, "uusd")); +// let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// // Using current time since start time wasn't provided +// let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; +// let end_expiration = expiration_from_milliseconds(Milliseconds(current_time + 2)).unwrap(); + +// let state = State { +// end_time: end_expiration, +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(5u128), +// max_amount_per_wallet: 2, +// amount_sold: Uint128::from(4u128), +// amount_to_send: Uint128::from(360u128), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }; +// assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); + +// assert_eq!( +// vec![ +// get_purchase(MOCK_TOKENS_FOR_SALE[0], "A"), +// get_purchase(MOCK_TOKENS_FOR_SALE[1], "A") +// ], +// PURCHASES.load(deps.as_ref().storage, "A").unwrap() +// ); + +// assert_eq!( +// vec![get_purchase(MOCK_TOKENS_FOR_SALE[2], "B"),], +// PURCHASES.load(deps.as_ref().storage, "B").unwrap() +// ); + +// assert_eq!( +// vec![get_purchase(MOCK_TOKENS_FOR_SALE[3], "C"),], +// PURCHASES.load(deps.as_ref().storage, "C").unwrap() +// ); +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[0])); +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[1])); +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[2])); +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[3])); + +// assert_eq!( +// NUMBER_OF_TOKENS_AVAILABLE +// .load(deps.as_ref().storage) +// .unwrap(), +// Uint128::new(3) +// ); + +// let mut env = mock_env(); +// env.block.time = env.block.time.plus_hours(1); + +// // User B claims their own refund. +// let msg = ExecuteMsg::ClaimRefund {}; +// let info = mock_info("B", &[]); +// let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); +// assert_eq!( +// Response::new() +// .add_attribute("action", "claim_refund") +// .add_message(CosmosMsg::Bank(BankMsg::Send { +// to_address: "B".to_string(), +// amount: coins(150, "uusd"), +// })) +// .add_submessage(generate_economics_message("B", "ClaimRefund")), +// res +// ); + +// assert!(!PURCHASES.has(deps.as_ref().storage, "B")); + +// env.contract.address = Addr::unchecked(MOCK_CONDITIONS_NOT_MET_CONTRACT); +// deps.querier.tokens_left_to_burn = 7; +// let msg = ExecuteMsg::EndSale { limit: None }; +// let info = mock_info("anyone", &[]); +// let res = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()).unwrap(); +// let refund_msgs: Vec = vec![ +// // All of A's payments grouped into one message. +// CosmosMsg::Bank(BankMsg::Send { +// to_address: "A".to_string(), +// amount: coins(300, "uusd"), +// }), +// CosmosMsg::Bank(BankMsg::Send { +// to_address: "C".to_string(), +// amount: coins(150, "uusd"), +// }), +// ]; +// let burn_msgs: Vec = vec![ +// get_burn_message(MOCK_TOKENS_FOR_SALE[0]), +// get_burn_message(MOCK_TOKENS_FOR_SALE[1]), +// get_burn_message(MOCK_TOKENS_FOR_SALE[2]), +// get_burn_message(MOCK_TOKENS_FOR_SALE[3]), +// // Tokens that were not sold. +// get_burn_message(MOCK_TOKENS_FOR_SALE[4]), +// get_burn_message(MOCK_TOKENS_FOR_SALE[5]), +// get_burn_message(MOCK_TOKENS_FOR_SALE[6]), +// ]; + +// assert_eq!( +// Response::new() +// .add_attribute("action", "issue_refunds_and_burn_tokens") +// .add_messages(refund_msgs) +// .add_messages(burn_msgs) +// .add_submessage(generate_economics_message("anyone", "EndSale")), +// res +// ); + +// assert!(!PURCHASES.has(deps.as_ref().storage, "A")); +// assert!(!PURCHASES.has(deps.as_ref().storage, "C")); + +// // Burned tokens have been removed. +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[4])); +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[5])); +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[6])); + +// deps.querier.tokens_left_to_burn = 0; +// let _res = execute(deps.as_mut(), env, info, msg).unwrap(); +// assert!(STATE.may_load(deps.as_mut().storage).unwrap().is_none()); +// assert_eq!( +// NUMBER_OF_TOKENS_AVAILABLE +// .load(deps.as_ref().storage) +// .unwrap(), +// Uint128::zero() +// ); +// } + +// #[test] +// fn test_integration_conditions_met() { +// let mut deps = mock_dependencies_custom(&[]); +// deps.querier.contract_address = MOCK_CONDITIONS_MET_CONTRACT.to_string(); +// let modules = vec![Module { +// name: Some(RATES.to_owned()), +// address: AndrAddr::from_string(MOCK_RATES_CONTRACT.to_owned()), +// is_mutable: false, +// }]; +// init(deps.as_mut(), Some(modules)); +// let mut env = mock_env(); +// env.contract.address = Addr::unchecked(MOCK_CONDITIONS_MET_CONTRACT); + +// // Mint all tokens. +// for &token_id in MOCK_TOKENS_FOR_SALE { +// let _res = mint(deps.as_mut(), token_id).unwrap(); +// assert!(AVAILABLE_TOKENS.has(deps.as_ref().storage, token_id)); +// } +// let current_time = mock_env().block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; + +// let msg = ExecuteMsg::StartSale { +// start_time: None, +// end_time: Expiry::AtTime(Milliseconds::from_nanos((current_time + 2) * 1_000_000)), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(3u128), +// max_amount_per_wallet: Some(2), +// recipient: Recipient::from_string("recipient"), +// }; + +// let info = mock_info("owner", &[]); +// let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: Some(1), +// }; +// let info = mock_info("A", &coins(150, "uusd")); +// let _res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: Some(1), +// }; +// let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: Some(1), +// }; +// let info = mock_info("B", &coins(150, "uusd")); +// let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: Some(1), +// }; +// let info = mock_info("C", &coins(150, "uusd")); +// let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: Some(1), +// }; +// let info = mock_info("D", &coins(150, "uusd")); +// let _res = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); +// // Using current time since start time wasn't provided +// let current_time = env.block.time.nanos() / MILLISECONDS_TO_NANOSECONDS_RATIO; +// let end_expiration = expiration_from_milliseconds(Milliseconds(current_time + 2)).unwrap(); +// let mut state = State { +// end_time: end_expiration, +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(3u128), +// max_amount_per_wallet: 2, +// amount_sold: Uint128::from(5u128), +// amount_to_send: Uint128::from(450u128), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }; +// assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); + +// assert_eq!( +// vec![ +// get_purchase(MOCK_TOKENS_FOR_SALE[0], "A"), +// get_purchase(MOCK_TOKENS_FOR_SALE[1], "A") +// ], +// PURCHASES.load(deps.as_ref().storage, "A").unwrap() +// ); + +// assert_eq!( +// vec![get_purchase(MOCK_TOKENS_FOR_SALE[2], "B"),], +// PURCHASES.load(deps.as_ref().storage, "B").unwrap() +// ); +// assert_eq!( +// vec![get_purchase(MOCK_TOKENS_FOR_SALE[3], "C"),], +// PURCHASES.load(deps.as_ref().storage, "C").unwrap() +// ); +// assert_eq!( +// vec![get_purchase(MOCK_TOKENS_FOR_SALE[4], "D"),], +// PURCHASES.load(deps.as_ref().storage, "D").unwrap() +// ); +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[0])); +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[1])); +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[2])); +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[3])); +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[4])); + +// env.block.time = env.block.time.plus_hours(1); +// env.contract.address = Addr::unchecked(MOCK_CONDITIONS_MET_CONTRACT); + +// let msg = ExecuteMsg::EndSale { limit: Some(1) }; +// let info = mock_info("anyone", &[]); +// let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + +// assert_eq!( +// Response::new() +// .add_attribute("action", "transfer_tokens_and_send_funds") +// .add_message(get_transfer_message( +// MOCK_TOKENS_FOR_SALE[0], +// AndrAddr::from_string("A") +// )) +// .add_submessages(get_rates_messages()) +// .add_submessage(generate_economics_message("anyone", "EndSale")), +// res +// ); + +// assert_eq!( +// vec![get_purchase(MOCK_TOKENS_FOR_SALE[1], "A")], +// PURCHASES.load(deps.as_ref().storage, "A").unwrap() +// ); + +// state.amount_transferred += Uint128::from(1u128); +// assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); + +// let msg = ExecuteMsg::EndSale { limit: Some(2) }; +// let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + +// assert_eq!( +// Response::new() +// .add_attribute("action", "transfer_tokens_and_send_funds") +// .add_message(get_transfer_message( +// MOCK_TOKENS_FOR_SALE[1], +// AndrAddr::from_string("A") +// )) +// .add_message(get_transfer_message( +// MOCK_TOKENS_FOR_SALE[2], +// AndrAddr::from_string("B") +// )) +// .add_message(CosmosMsg::Bank(BankMsg::Send { +// to_address: MOCK_ROYALTY_RECIPIENT.to_owned(), +// amount: vec![Coin { +// // Royalty of 10% for A and B combined +// amount: Uint128::from(20u128), +// denom: "uusd".to_string(), +// }], +// })) +// .add_message(CosmosMsg::Bank(BankMsg::Send { +// to_address: MOCK_TAX_RECIPIENT.to_owned(), +// amount: vec![Coin { +// // Combined tax for both A and B +// amount: Uint128::from(100u128), +// denom: "uusd".to_string(), +// }], +// })) +// .add_submessage(generate_economics_message("anyone", "EndSale")), +// res +// ); + +// assert!(!PURCHASES.has(deps.as_ref().storage, "A"),); +// assert!(!PURCHASES.has(deps.as_ref().storage, "B"),); +// assert!(PURCHASES.has(deps.as_ref().storage, "C"),); +// assert!(PURCHASES.has(deps.as_ref().storage, "D"),); + +// state.amount_transferred += Uint128::from(2u128); +// assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); + +// let msg = ExecuteMsg::EndSale { limit: None }; +// let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + +// assert!(!PURCHASES.has(deps.as_ref().storage, "C"),); +// assert!(!PURCHASES.has(deps.as_ref().storage, "D"),); + +// assert_eq!( +// Response::new() +// .add_attribute("action", "transfer_tokens_and_send_funds") +// .add_message(get_transfer_message( +// MOCK_TOKENS_FOR_SALE[3], +// AndrAddr::from_string("C") +// )) +// .add_message(get_transfer_message( +// MOCK_TOKENS_FOR_SALE[4], +// AndrAddr::from_string("D") +// )) +// .add_message(CosmosMsg::Bank(BankMsg::Send { +// to_address: MOCK_ROYALTY_RECIPIENT.to_owned(), +// amount: vec![Coin { +// // Royalty of 10% for C and D combined +// amount: Uint128::from(20u128), +// denom: "uusd".to_string(), +// }], +// })) +// .add_message(CosmosMsg::Bank(BankMsg::Send { +// to_address: MOCK_TAX_RECIPIENT.to_owned(), +// amount: vec![Coin { +// // Combined tax for both C and D +// amount: Uint128::from(100u128), +// denom: "uusd".to_string(), +// }], +// })) +// .add_submessage(generate_economics_message("anyone", "EndSale")), +// res +// ); + +// state.amount_transferred += Uint128::from(2u128); +// assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); + +// let msg = ExecuteMsg::EndSale { limit: None }; +// let res = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()).unwrap(); +// // Added one for economics message +// assert_eq!(3 + 1, res.messages.len()); + +// // assert_eq!( +// // Response::new() +// // .add_attribute("action", "transfer_tokens_and_send_funds") +// // // Now that all tokens have been transfered, can send the funds to recipient. +// // .add_message(CosmosMsg::Bank(BankMsg::Send { +// // to_address: "recipient".to_string(), +// // amount: coins(450u128, "uusd") +// // })) +// // // Burn tokens that were not purchased +// // .add_message(get_burn_message(MOCK_TOKENS_FOR_SALE[5])) +// // .add_message(get_burn_message(MOCK_TOKENS_FOR_SALE[6])), +// // res +// // ); + +// state.amount_to_send = Uint128::zero(); +// assert_eq!(state, STATE.load(deps.as_ref().storage).unwrap()); + +// // Burned tokens removed. +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[5])); +// assert!(!AVAILABLE_TOKENS.has(deps.as_ref().storage, MOCK_TOKENS_FOR_SALE[6])); + +// deps.querier.tokens_left_to_burn = 0; +// let _res = execute(deps.as_mut(), env, info, msg).unwrap(); +// assert!(STATE.may_load(deps.as_mut().storage).unwrap().is_none()); +// assert_eq!( +// NUMBER_OF_TOKENS_AVAILABLE +// .load(deps.as_ref().storage) +// .unwrap(), +// Uint128::zero() +// ); +// } + +// #[test] +// fn test_end_sale_single_purchase() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// STATE +// .save( +// deps.as_mut().storage, +// &State { +// end_time: Expiration::AtHeight(mock_env().block.height - 1), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 5, +// amount_sold: Uint128::from(1u128), +// amount_to_send: Uint128::from(100u128), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }, +// ) +// .unwrap(); + +// PURCHASES +// .save( +// deps.as_mut().storage, +// "A", +// &vec![Purchase { +// token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), +// purchaser: "A".to_string(), +// tax_amount: Uint128::zero(), +// msgs: vec![], +// }], +// ) +// .unwrap(); + +// let msg = ExecuteMsg::EndSale { limit: None }; +// let info = mock_info("anyone", &[]); +// let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// assert_eq!( +// Response::new() +// .add_attribute("action", "transfer_tokens_and_send_funds") +// // Burn tokens that were not purchased +// .add_message(get_transfer_message( +// MOCK_TOKENS_FOR_SALE[0], +// AndrAddr::from_string("A") +// )) +// .add_submessage(generate_economics_message("anyone", "EndSale")), +// res +// ); +// } + +// #[test] +// fn test_end_sale_all_tokens_sold() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// STATE +// .save( +// deps.as_mut().storage, +// &State { +// // Sale has not expired yet. +// end_time: Expiration::AtHeight(mock_env().block.height + 1), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 5, +// amount_sold: Uint128::from(1u128), +// amount_to_send: Uint128::from(100u128), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }, +// ) +// .unwrap(); + +// PURCHASES +// .save( +// deps.as_mut().storage, +// "A", +// &vec![Purchase { +// token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), +// purchaser: "A".to_string(), +// tax_amount: Uint128::zero(), +// msgs: vec![], +// }], +// ) +// .unwrap(); + +// NUMBER_OF_TOKENS_AVAILABLE +// .save(deps.as_mut().storage, &Uint128::zero()) +// .unwrap(); + +// let msg = ExecuteMsg::EndSale { limit: None }; +// let info = mock_info("anyone", &[]); +// let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// assert_eq!( +// Response::new() +// .add_attribute("action", "transfer_tokens_and_send_funds") +// // Burn tokens that were not purchased +// .add_message(get_transfer_message( +// MOCK_TOKENS_FOR_SALE[0], +// AndrAddr::from_string("A") +// )) +// .add_submessage(generate_economics_message("anyone", "EndSale")), +// res +// ); +// } + +// #[test] +// fn test_end_sale_some_tokens_sold_threshold_met() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// STATE +// .save( +// deps.as_mut().storage, +// &State { +// // Sale has not expired yet. +// end_time: Expiration::AtHeight(mock_env().block.height + 1), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 5, +// amount_sold: Uint128::from(2u128), +// amount_to_send: Uint128::from(100u128), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }, +// ) +// .unwrap(); + +// PURCHASES +// .save( +// deps.as_mut().storage, +// "A", +// &vec![Purchase { +// token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), +// purchaser: "A".to_string(), +// tax_amount: Uint128::zero(), +// msgs: vec![], +// }], +// ) +// .unwrap(); + +// NUMBER_OF_TOKENS_AVAILABLE +// .save(deps.as_mut().storage, &Uint128::one()) +// .unwrap(); + +// let msg = ExecuteMsg::EndSale { limit: None }; +// // Only the owner can end the sale if only the minimum token threshold is met. +// // Anyone can end the sale if it's expired or the remaining number of tokens available is zero. +// let info = mock_info("anyone", &[]); +// let err = execute(deps.as_mut(), mock_env(), info, msg.clone()).unwrap_err(); +// assert_eq!(err, ContractError::SaleNotEnded {}); + +// let info = mock_info("owner", &[]); +// let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// assert_eq!( +// Response::new() +// .add_attribute("action", "transfer_tokens_and_send_funds") +// // Burn tokens that were not purchased +// .add_message(get_transfer_message( +// MOCK_TOKENS_FOR_SALE[0], +// AndrAddr::from_string("A") +// )) +// .add_submessage(generate_economics_message("owner", "EndSale")), +// res +// ); +// } + +// #[test] +// fn test_end_sale_some_tokens_sold_threshold_not_met() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// STATE +// .save( +// deps.as_mut().storage, +// &State { +// // Sale has not expired yet. +// end_time: Expiration::AtHeight(mock_env().block.height + 1), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(2u128), +// max_amount_per_wallet: 5, +// amount_sold: Uint128::from(0u128), +// amount_to_send: Uint128::from(100u128), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }, +// ) +// .unwrap(); + +// PURCHASES +// .save( +// deps.as_mut().storage, +// "A", +// &vec![Purchase { +// token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), +// purchaser: "A".to_string(), +// tax_amount: Uint128::zero(), +// msgs: vec![], +// }], +// ) +// .unwrap(); + +// NUMBER_OF_TOKENS_AVAILABLE +// .save(deps.as_mut().storage, &Uint128::new(2)) +// .unwrap(); + +// let msg = ExecuteMsg::EndSale { limit: None }; + +// let info = mock_info("owner", &[]); +// // Minimum sold is 2, actual sold is 0 +// let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); +// assert_eq!(err, ContractError::SaleNotEnded {}); +// } + +// #[test] +// fn test_end_sale_limit_zero() { +// let mut deps = mock_dependencies_custom(&[]); +// init(deps.as_mut(), None); + +// STATE +// .save( +// deps.as_mut().storage, +// &State { +// end_time: Expiration::AtHeight(mock_env().block.height - 1), +// price: coin(100, "uusd"), +// min_tokens_sold: Uint128::from(1u128), +// max_amount_per_wallet: 5, +// amount_sold: Uint128::from(1u128), +// amount_to_send: Uint128::from(100u128), +// amount_transferred: Uint128::zero(), +// recipient: Recipient::from_string("recipient"), +// }, +// ) +// .unwrap(); +// NUMBER_OF_TOKENS_AVAILABLE +// .save(deps.as_mut().storage, &Uint128::new(1)) +// .unwrap(); + +// PURCHASES +// .save( +// deps.as_mut().storage, +// "A", +// &vec![Purchase { +// token_id: MOCK_TOKENS_FOR_SALE[0].to_owned(), +// purchaser: "A".to_string(), +// tax_amount: Uint128::zero(), +// msgs: vec![], +// }], +// ) +// .unwrap(); + +// let msg = ExecuteMsg::EndSale { limit: Some(0) }; +// let info = mock_info("anyone", &[]); +// let res = execute(deps.as_mut(), mock_env(), info, msg); + +// assert_eq!(ContractError::LimitMustNotBeZero {}, res.unwrap_err()); +// } + +// #[test] +// fn test_validate_andr_addresses_regular_address() { +// let mut deps = mock_dependencies_custom(&[]); +// let msg = InstantiateMsg { +// token_address: AndrAddr::from_string("terra1asdf1ssdfadf".to_owned()), +// owner: None, +// modules: None, +// can_mint_after_sale: true, +// kernel_address: MOCK_KERNEL_CONTRACT.to_string(), +// }; + +// let info = mock_info("owner", &[]); +// let _res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + +// let msg = ExecuteMsg::UpdateAppContract { +// address: MOCK_APP_CONTRACT.to_owned(), +// }; + +// let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// assert_eq!( +// Response::new() +// .add_attribute("action", "update_app_contract") +// .add_attribute("address", MOCK_APP_CONTRACT) +// .add_submessage(generate_economics_message("owner", "UpdateAppContract")), +// res +// ); +// } + +// #[test] +// fn test_addresslist() { +// let mut deps = mock_dependencies_custom(&[]); +// let modules = vec![Module { +// name: Some(ADDRESS_LIST.to_owned()), +// address: AndrAddr::from_string(MOCK_ADDRESS_LIST_CONTRACT.to_owned()), +// is_mutable: false, +// }]; +// let msg = InstantiateMsg { +// token_address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), +// modules: Some(modules), +// can_mint_after_sale: true, +// owner: None, +// kernel_address: MOCK_KERNEL_CONTRACT.to_string(), +// }; + +// let info = mock_info("owner", &[]); +// let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// // Not whitelisted user +// let msg = ExecuteMsg::Purchase { +// number_of_tokens: None, +// }; +// let info = mock_info("not_whitelisted", &[]); +// let res = execute(deps.as_mut(), mock_env(), info, msg); + +// assert_eq!( +// ContractError::Std(StdError::generic_err( +// "Querier contract error: InvalidAddress" +// )), +// res.unwrap_err() +// ); +// } + +// #[test] +// fn test_update_token_contract() { +// let mut deps = mock_dependencies_custom(&[]); +// let msg = InstantiateMsg { +// token_address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), +// modules: None, +// can_mint_after_sale: true, +// owner: None, +// kernel_address: MOCK_KERNEL_CONTRACT.to_string(), +// }; + +// let info = mock_info("owner", &[]); +// let _res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + +// let msg = ExecuteMsg::UpdateTokenContract { +// address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), +// }; + +// let res = execute(deps.as_mut(), mock_env(), info, msg); +// assert!(res.is_ok()) +// } + +// #[test] +// fn test_update_token_contract_unauthorized() { +// let mut deps = mock_dependencies_custom(&[]); +// let msg = InstantiateMsg { +// token_address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), +// modules: None, +// can_mint_after_sale: true, +// owner: None, +// kernel_address: MOCK_KERNEL_CONTRACT.to_string(), +// }; + +// let info = mock_info("app_contract", &[]); +// let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + +// let msg = ExecuteMsg::UpdateTokenContract { +// address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), +// }; + +// let unauth_info = mock_info("attacker", &[]); +// let res = execute(deps.as_mut(), mock_env(), unauth_info, msg).unwrap_err(); +// assert_eq!(ContractError::Unauthorized {}, res); +// } + +// #[test] +// fn test_update_token_contract_post_mint() { +// let mut deps = mock_dependencies_custom(&[]); +// let msg = InstantiateMsg { +// token_address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), +// modules: None, +// can_mint_after_sale: true, +// owner: None, +// kernel_address: MOCK_KERNEL_CONTRACT.to_string(), +// }; + +// let info = mock_info("owner", &[]); +// let _res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + +// mint(deps.as_mut(), "1").unwrap(); + +// let msg = ExecuteMsg::UpdateTokenContract { +// address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), +// }; + +// let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); +// assert_eq!(ContractError::Unauthorized {}, res); +// } + +// #[test] +// fn test_update_token_contract_not_cw721() { +// let mut deps = mock_dependencies_custom(&[]); +// let msg = InstantiateMsg { +// token_address: AndrAddr::from_string(MOCK_TOKEN_CONTRACT.to_owned()), +// modules: None, +// can_mint_after_sale: true, +// owner: None, +// kernel_address: MOCK_KERNEL_CONTRACT.to_string(), +// }; + +// let info = mock_info("owner", &[]); +// let _res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); + +// let msg = ExecuteMsg::UpdateTokenContract { +// address: AndrAddr::from_string("not_a_token_contract".to_owned()), +// }; + +// let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); +// assert_eq!(ContractError::Unauthorized {}, res); +// } diff --git a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs index 4e567aba0..c92ac2ba8 100644 --- a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs +++ b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs @@ -1,114 +1,89 @@ -use crate::cw721::TokenExtension; -use andromeda_std::amp::{addresses::AndrAddr, recipient::Recipient}; -use andromeda_std::common::expiration::Expiry; +use andromeda_std::amp::addresses::AndrAddr; +use andromeda_std::common::denom::validate_denom; +use andromeda_std::error::ContractError; use andromeda_std::{andr_exec, andr_instantiate, andr_instantiate_modules, andr_query}; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Coin, Uint128}; -use cw721::Expiration; +use cosmwasm_std::{ensure, Deps, Uint128}; #[andr_instantiate] #[andr_instantiate_modules] #[cw_serde] pub struct InstantiateMsg { - pub token_address: AndrAddr, - pub can_mint_after_sale: bool, + /// The configuration for the campaign + pub campaign_config: CampaignConfig, } #[andr_exec] #[cw_serde] -pub enum ExecuteMsg { - /// Mints a new token to be sold in a future sale. Only possible when the sale is not ongoing. - Mint(Vec), - /// Starts the sale if one is not already ongoing. - StartSale { - /// When the sale start. Defaults to current time. - start_time: Option, - /// When the sale ends. - end_time: Expiry, - /// The price per token. - price: Coin, - /// The minimum amount of tokens sold to go through with the sale. - min_tokens_sold: Uint128, - /// The amount of tokens a wallet can purchase, default is 1. - max_amount_per_wallet: Option, - /// The recipient of the funds if the sale met the minimum sold. - recipient: Recipient, - }, - /// Updates the token address to a new one. - /// Only accessible by owner - UpdateTokenContract { address: AndrAddr }, - /// Puchases tokens in an ongoing sale. - Purchase { number_of_tokens: Option }, - /// Purchases the token with the given id. - PurchaseByTokenId { token_id: String }, - /// Allow a user to claim their own refund if the minimum number of tokens are not sold. - ClaimRefund {}, - /// Ends the ongoing sale by completing `limit` number of operations depending on if the minimum number - /// of tokens was sold. - EndSale { limit: Option }, -} +pub enum ExecuteMsg {} #[andr_query] #[cw_serde] #[derive(QueryResponses)] -pub enum QueryMsg { - #[returns(State)] - State {}, - #[returns(Config)] - Config {}, - #[returns(Vec)] - AvailableTokens { - start_after: Option, - limit: Option, - }, - #[returns(IsTokenAvailableResponse)] - IsTokenAvailable { id: String }, -} +pub enum QueryMsg {} #[cw_serde] -pub struct IsTokenAvailableResponse { - pub is_token_available: bool, +pub struct CampaignConfig { + /// Title of the campaign. Maximum length is 32. + pub title: String, + /// Short description about the campaign. Maximum length is 256. + pub description: String, + /// URL for the banner of the campaign + pub banner: String, + /// Official website of the campaign + pub url: String, + /// Withdrawal address for the funds gained by the campaign + pub tier_address: AndrAddr, + /// The address of the tier contract whose tokens are being distributed + pub denom: String, + /// The minimum amount of funding to be sold for the successful fundraising + pub withdrawal_address: AndrAddr, + /// The address of the tier contract whose tokens are being distributed + pub soft_cap: Option, + /// The maximum amount of funding to be sold for the fundraising + pub hard_cap: Uint128, } -#[cw_serde] -pub struct Config { - /// The address of the token contract whose tokens are being sold. - pub token_address: AndrAddr, - /// Whether or not the owner can mint additional tokens after the sale has been conducted. - pub can_mint_after_sale: bool, -} +impl CampaignConfig { + pub fn validate(&self, deps: Deps) -> Result<(), ContractError> { + // validate addresses + self.tier_address.validate(deps.api)?; + self.withdrawal_address.validate(deps.api)?; + validate_denom(deps, self.denom.clone())?; -#[cw_serde] -pub struct State { - /// The expiration denoting when the sale ends. - pub end_time: Expiration, - /// The price of each token. - pub price: Coin, - /// The minimum number of tokens sold for the sale to go through. - pub min_tokens_sold: Uint128, - /// The max number of tokens allowed per wallet. - pub max_amount_per_wallet: u32, - /// Number of tokens sold. - pub amount_sold: Uint128, - /// The amount of funds to send to recipient if sale successful. This already - /// takes into account the royalties and taxes. - pub amount_to_send: Uint128, - /// Number of tokens transferred to purchasers if sale was successful. - pub amount_transferred: Uint128, - /// The recipient of the raised funds if the sale is successful. - pub recipient: Recipient, + // validate meta info + ensure!( + self.title.len() <= 32, + ContractError::InvalidParameter { + error: Some("Title length can be 32 at maximum".to_string()) + } + ); + ensure!( + self.description.len() <= 256, + ContractError::InvalidParameter { + error: Some("Description length can be 256 at maximum".to_string()) + } + ); + + // validate target capital + ensure!( + (self.soft_cap).map_or(true, |soft_cap| soft_cap < self.hard_cap), + ContractError::InvalidParameter { + error: Some("soft_cap can not exceed hard_cap".to_string()) + } + ); + Ok(()) + } } #[cw_serde] -pub struct CrowdfundMintMsg { - /// Unique ID of the NFT - pub token_id: String, - /// The owner of the newly minter NFT - pub owner: Option, - /// Universal resource identifier for this NFT - /// Should point to a JSON file that conforms to the ERC721 - /// Metadata JSON Schema - pub token_uri: Option, - /// Any custom extension used by this contract - pub extension: TokenExtension, +pub enum CampaignStage { + /// Stage when all necessary environment is set to start campaign + READY, + /// Stage when campaign is being carried out + ONGOING, + /// Stage when campaign is finished successfully + SUCCEED, + /// Stage when campaign failed to meet the target cap before expiration + FAILED, } diff --git a/packages/std/src/error.rs b/packages/std/src/error.rs index b22d27155..478570a2c 100644 --- a/packages/std/src/error.rs +++ b/packages/std/src/error.rs @@ -630,6 +630,9 @@ pub enum ContractError { #[error("Invalid Expiration Time")] InvalidExpirationTime {}, + #[error("Invalid Parameter, {error:?}")] + InvalidParameter { error: Option }, + #[error("Invalid Pathname, {error:?}")] InvalidPathname { error: Option }, diff --git a/tests-integration/tests/crowdfund_app.rs b/tests-integration/tests/crowdfund_app.rs deleted file mode 100644 index 590e7ae64..000000000 --- a/tests-integration/tests/crowdfund_app.rs +++ /dev/null @@ -1,249 +0,0 @@ -use andromeda_app::app::{AppComponent, ComponentType}; -use andromeda_app_contract::mock::{mock_andromeda_app, MockAppContract}; -use andromeda_crowdfund::mock::{ - mock_andromeda_crowdfund, mock_crowdfund_instantiate_msg, mock_query_ado_base_version, - MockCrowdfund, -}; -use andromeda_cw721::mock::{mock_andromeda_cw721, mock_cw721_instantiate_msg, MockCW721}; -use andromeda_finance::splitter::AddressPercent; -use andromeda_std::{ - ado_base::version::ADOBaseVersionResponse, - amp::{AndrAddr, Recipient}, - common::{expiration::Expiry, Milliseconds}, -}; - -use andromeda_modules::rates::{Rate, RateInfo}; -use andromeda_rates::mock::{mock_andromeda_rates, mock_rates_instantiate_msg}; -use andromeda_splitter::mock::{ - mock_andromeda_splitter, mock_splitter_instantiate_msg, mock_splitter_send_msg, -}; -use andromeda_std::ado_base::modules::Module; -use std::str::FromStr; - -use andromeda_testing::{ - mock::mock_app, mock_builder::MockAndromedaBuilder, mock_contract::MockContract, -}; -use andromeda_vault::mock::mock_andromeda_vault; -use cosmwasm_std::{coin, to_json_binary, BlockInfo, Decimal, Uint128}; -use cw_multi_test::Executor; - -#[test] -fn test_crowdfund_app() { - let mut router = mock_app(None); - let andr = MockAndromedaBuilder::new(&mut router, "admin") - .with_wallets(vec![ - ("owner", vec![]), - ("vault_one_recipient", vec![]), - ("vault_two_recipient", vec![]), - ("buyer_one", vec![coin(100, "uandr")]), - ("buyer_two", vec![coin(100, "uandr")]), - ("buyer_three", vec![coin(100, "uandr")]), - ("rates_recipient", vec![]), - ]) - .with_contracts(vec![ - ("cw721", mock_andromeda_cw721()), - ("crowdfund", mock_andromeda_crowdfund()), - ("vault", mock_andromeda_vault()), - ("splitter", mock_andromeda_splitter()), - ("app-contract", mock_andromeda_app()), - ("rates", mock_andromeda_rates()), - ]) - .build(&mut router); - - let owner = andr.get_wallet("owner"); - let vault_one_recipient_addr = andr.get_wallet("vault_one_recipient"); - let vault_two_recipient_addr = andr.get_wallet("vault_two_recipient"); - let buyer_one = andr.get_wallet("buyer_one"); - let buyer_two = andr.get_wallet("buyer_two"); - let buyer_three = andr.get_wallet("buyer_three"); - - // Store contract codes - let app_code_id = andr.get_code_id(&mut router, "app-contract"); - let rates_code_id = andr.get_code_id(&mut router, "rates"); - - // Generate App Components - // App component names must be less than 3 characters or longer than 54 characters to force them to be 'invalid' as the MockApi struct used within the CosmWasm App struct only contains those two validation checks - let rates_recipient = andr.get_wallet("rates_recipient"); - // Generate rates contract - let rates: Vec = [RateInfo { - rate: Rate::Flat(coin(1, "uandr")), - is_additive: false, - recipients: [Recipient::from_string(rates_recipient.to_string())].to_vec(), - description: Some("Some test rate".to_string()), - }] - .to_vec(); - let rates_init_msg = mock_rates_instantiate_msg(rates, andr.kernel.addr().to_string(), None); - let rates_addr = router - .instantiate_contract( - rates_code_id, - owner.clone(), - &rates_init_msg, - &[], - "rates", - None, - ) - .unwrap(); - - let modules: Vec = vec![Module::new("rates", rates_addr.to_string(), false)]; - - let crowdfund_app_component = AppComponent { - name: "crowdfund".to_string(), - ado_type: "crowdfund".to_string(), - component_type: ComponentType::New( - to_json_binary(&mock_crowdfund_instantiate_msg( - AndrAddr::from_string("./tokens"), - false, - Some(modules), - andr.kernel.addr().to_string(), - None, - )) - .unwrap(), - ), - }; - let cw721_component = AppComponent { - name: "tokens".to_string(), - ado_type: "cw721".to_string(), - component_type: ComponentType::new(mock_cw721_instantiate_msg( - "Test Tokens".to_string(), - "TT".to_string(), - format!("./{}", crowdfund_app_component.name), // Crowdfund must be minter - None, - andr.kernel.addr().to_string(), - None, - )), - }; - - let splitter_recipients = vec![ - AddressPercent { - recipient: Recipient::from_string(format!("~{vault_one_recipient_addr}")), - percent: Decimal::from_str("0.5").unwrap(), - }, - AddressPercent { - recipient: Recipient::from_string(vault_two_recipient_addr), - percent: Decimal::from_str("0.5").unwrap(), - }, - ]; - - let splitter_init_msg = - mock_splitter_instantiate_msg(splitter_recipients, andr.kernel.addr().clone(), None, None); - let splitter_app_component = AppComponent { - name: "split".to_string(), - component_type: ComponentType::new(splitter_init_msg), - ado_type: "splitter".to_string(), - }; - - let app_components = vec![ - cw721_component.clone(), - crowdfund_app_component.clone(), - splitter_app_component.clone(), - ]; - - let app = MockAppContract::instantiate( - app_code_id, - owner, - &mut router, - "app-contract", - app_components.clone(), - andr.kernel.addr().clone(), - Some(owner.to_string()), - ); - - let components = app.query_components(&router); - assert_eq!(components, app_components); - - let cw721_contract = - app.query_ado_by_component_name::(&router, cw721_component.name); - let crowdfund_contract = - app.query_ado_by_component_name::(&router, crowdfund_app_component.name); - - let minter = cw721_contract.query_minter(&router); - assert_eq!(minter, crowdfund_contract.addr()); - - // Mint Tokens - crowdfund_contract - .execute_quick_mint(owner.clone(), &mut router, 5, owner.to_string()) - .unwrap(); - - // Start Sale - let token_price = coin(100, "uandr"); - - let sale_recipient = - Recipient::from_string(format!("~{}/{}", app.addr(), splitter_app_component.name)) - .with_msg(mock_splitter_send_msg()); - let start_time = Expiry::AtTime( - Milliseconds::from_seconds(router.block_info().time.seconds()).plus_seconds(1), - ); - let end_time = Expiry::AtTime(start_time.get_time(&router.block_info()).plus_seconds(5)); - crowdfund_contract - .execute_start_sale( - owner.clone(), - &mut router, - Some(start_time), - end_time.clone(), - token_price.clone(), - Uint128::from(3u128), - Some(1), - sale_recipient, - ) - .unwrap(); - - // Buy Tokens - let buyers = vec![buyer_one, buyer_two, buyer_three]; - for buyer in buyers.clone() { - crowdfund_contract - .execute_purchase(buyer.clone(), &mut router, Some(1), &[token_price.clone()]) - .unwrap(); - } - let crowdfund_balance = router - .wrap() - .query_balance(crowdfund_contract.addr().clone(), token_price.denom) - .unwrap(); - assert_eq!(crowdfund_balance.amount, Uint128::from(300u128)); - - // End Sale - let block_info = router.block_info(); - router.set_block(BlockInfo { - height: block_info.height, - time: end_time - .get_time(&router.block_info()) - .plus_seconds(1) - .into(), - chain_id: block_info.chain_id, - }); - - crowdfund_contract - .execute_end_sale(owner.clone(), &mut router, None) - .unwrap(); - crowdfund_contract - .execute_end_sale(owner.clone(), &mut router, None) - .unwrap(); - - // Check final state - //Check token transfers - for (i, buyer) in buyers.iter().enumerate() { - let owner = cw721_contract.query_owner_of(&router, i.to_string()); - assert_eq!(owner, buyer.to_string()); - } - - let balance_one = router - .wrap() - .query_balance(vault_one_recipient_addr, "uandr") - .unwrap(); - assert_eq!(balance_one.amount, Uint128::from(148u128)); - - let balance_two = router - .wrap() - .query_balance(vault_two_recipient_addr, "uandr") - .unwrap(); - assert_eq!(balance_two.amount, Uint128::from(148u128)); - - let ado_base_version: ADOBaseVersionResponse = router - .wrap() - .query_wasm_smart( - crowdfund_contract.addr().clone(), - &mock_query_ado_base_version(), - ) - .unwrap(); - - assert_eq!(ado_base_version.version, "1.0.0".to_string()) -} diff --git a/tests-integration/tests/mod.rs b/tests-integration/tests/mod.rs index c7d39f330..f95204f6d 100644 --- a/tests-integration/tests/mod.rs +++ b/tests-integration/tests/mod.rs @@ -10,9 +10,6 @@ mod auction_app; #[cfg(test)] mod kernel; -#[cfg(test)] -mod crowdfund_app; - #[cfg(test)] mod cw20_staking; From 38589789022ffa166784d8469d81e91a829dea77 Mon Sep 17 00:00:00 2001 From: cowboy0015 Date: Thu, 2 May 2024 16:31:49 -0400 Subject: [PATCH 02/11] feat: implemented tier management feature --- .../andromeda-crowdfund/src/contract.rs | 139 ++++++++++++++---- .../andromeda-crowdfund/src/mock.rs | 8 +- .../andromeda-crowdfund/src/state.rs | 86 ++++++++++- .../src/testing/mock_querier.rs | 24 ++- .../andromeda-crowdfund/src/testing/tests.rs | 4 +- .../src/crowdfund.rs | 62 +++++++- packages/std/src/error.rs | 8 + 7 files changed, 290 insertions(+), 41 deletions(-) diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs index 976ea1d0a..758a96d5e 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs @@ -3,7 +3,7 @@ // PURCHASES, SALE_CONDUCTED, STATE, // }; use andromeda_non_fungible_tokens::crowdfund::{ - CampaignConfig, ExecuteMsg, InstantiateMsg, QueryMsg, + CampaignStage, ExecuteMsg, InstantiateMsg, QueryMsg, Tier, }; use andromeda_std::{ado_base::ownership::OwnershipMessage, common::actions::call_action}; use andromeda_std::{ado_contract::ADOContract, common::context::ExecuteContext}; @@ -16,9 +16,13 @@ use andromeda_std::{ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdError}; +use cosmwasm_std::{ + ensure, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdError, Uint64, +}; -use crate::state::CAMPAIGN_CONFIG; +use crate::state::{ + add_tier, get_current_stage, remove_tier, set_tiers, update_config, update_tier, +}; const CONTRACT_NAME: &str = "crates.io:andromeda-crowdfund"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -30,9 +34,10 @@ pub fn instantiate( info: MessageInfo, msg: InstantiateMsg, ) -> Result { - let config: CampaignConfig = msg.campaign_config; + update_config(deps.storage, msg.campaign_config)?; + + set_tiers(deps.storage, msg.tiers)?; - CAMPAIGN_CONFIG.save(deps.storage, &config)?; let inst_resp = ADOContract::default().instantiate( deps.storage, env, @@ -111,38 +116,110 @@ pub fn handle_execute(mut ctx: ExecuteContext, msg: ExecuteMsg) -> Result execute_add_tier(ctx, tier), + ExecuteMsg::UpdateTier { tier } => execute_update_tier(ctx, tier), + ExecuteMsg::RemoveTier { level } => execute_remove_tier(ctx, level), + _ => ADOContract::default().execute(ctx, msg), + }?; - let res = ADOContract::default().execute(ctx, msg)?; Ok(res .add_submessages(action_response.messages) .add_attributes(action_response.attributes) .add_events(action_response.events)) +} + +fn execute_add_tier(ctx: ExecuteContext, tier: Tier) -> Result { + let ExecuteContext { deps, info, .. } = ctx; + + let contract = ADOContract::default(); + ensure!( + contract.is_contract_owner(deps.storage, info.sender.as_str())?, + ContractError::Unauthorized {} + ); + + tier.validate()?; + + let curr_stage = get_current_stage(deps.storage); + ensure!( + curr_stage == CampaignStage::READY, + ContractError::InvalidCampaignOperation { + operation: "add_tier".to_string(), + stage: curr_stage.to_string() + } + ); + + add_tier(deps.storage, &tier)?; + + let mut resp = Response::new() + .add_attribute("action", "add_tier") + .add_attribute("level", tier.level) + .add_attribute("price", tier.price); + + if let Some(limit) = tier.limit { + resp = resp.add_attribute("limit", limit.to_string()); + } + + Ok(resp) +} + +fn execute_update_tier(ctx: ExecuteContext, tier: Tier) -> Result { + let ExecuteContext { deps, info, .. } = ctx; + + let contract = ADOContract::default(); + ensure!( + contract.is_contract_owner(deps.storage, info.sender.as_str())?, + ContractError::Unauthorized {} + ); + + tier.validate()?; + + let curr_stage = get_current_stage(deps.storage); + ensure!( + curr_stage == CampaignStage::READY, + ContractError::InvalidCampaignOperation { + operation: "update_tier".to_string(), + stage: curr_stage.to_string() + } + ); + + update_tier(deps.storage, &tier)?; + + let mut resp = Response::new() + .add_attribute("action", "update_tier") + .add_attribute("level", tier.level) + .add_attribute("price", tier.price); + + if let Some(limit) = tier.limit { + resp = resp.add_attribute("limit", limit.to_string()); + } + + Ok(resp) +} + +fn execute_remove_tier(ctx: ExecuteContext, level: Uint64) -> Result { + let ExecuteContext { deps, info, .. } = ctx; + + let contract = ADOContract::default(); + ensure!( + contract.is_contract_owner(deps.storage, info.sender.as_str())?, + ContractError::Unauthorized {} + ); + + let curr_stage = get_current_stage(deps.storage); + ensure!( + curr_stage == CampaignStage::READY, + ContractError::InvalidCampaignOperation { + operation: "remove_tier".to_string(), + stage: curr_stage.to_string() + } + ); + + remove_tier(deps.storage, level.into())?; + + let resp = Response::new().add_attribute("action", "remove_tier"); - // let res = match msg { - // ExecuteMsg::Mint(mint_msgs) => execute_mint(ctx, mint_msgs), - // ExecuteMsg::StartSale { - // start_time, - // end_time, - // price, - // min_tokens_sold, - // max_amount_per_wallet, - // recipient, - // } => execute_start_sale( - // ctx, - // start_time, - // end_time, - // price, - // min_tokens_sold, - // max_amount_per_wallet, - // recipient, - // ), - // ExecuteMsg::Purchase { number_of_tokens } => execute_purchase(ctx, number_of_tokens), - // ExecuteMsg::PurchaseByTokenId { token_id } => execute_purchase_by_token_id(ctx, token_id), - // ExecuteMsg::ClaimRefund {} => execute_claim_refund(ctx), - // ExecuteMsg::EndSale { limit } => execute_end_sale(ctx, limit), - // ExecuteMsg::UpdateTokenContract { address } => execute_update_token_contract(ctx, address), - // _ => ADOContract::default().execute(ctx, msg), - // }?; + Ok(resp) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/mock.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/mock.rs index fee758a64..eab68d18e 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/mock.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/mock.rs @@ -2,7 +2,7 @@ use crate::contract::{execute, instantiate, query, reply}; use andromeda_non_fungible_tokens::crowdfund::{ - CampaignConfig, ExecuteMsg, InstantiateMsg, QueryMsg, + CampaignConfig, ExecuteMsg, InstantiateMsg, QueryMsg, Tier, }; use andromeda_std::ado_base::modules::Module; use andromeda_testing::{ @@ -23,11 +23,13 @@ impl MockCrowdfund { sender: Addr, app: &mut MockApp, campaign_config: CampaignConfig, + tiers: Vec, modules: Option>, kernel_address: impl Into, owner: Option, ) -> MockCrowdfund { - let msg = mock_crowdfund_instantiate_msg(campaign_config, modules, kernel_address, owner); + let msg = + mock_crowdfund_instantiate_msg(campaign_config, tiers, modules, kernel_address, owner); let addr = app .instantiate_contract( code_id, @@ -119,12 +121,14 @@ pub fn mock_andromeda_crowdfund() -> Box> { pub fn mock_crowdfund_instantiate_msg( campaign_config: CampaignConfig, + tiers: Vec, modules: Option>, kernel_address: impl Into, owner: Option, ) -> InstantiateMsg { InstantiateMsg { campaign_config, + tiers, modules, kernel_address: kernel_address.into(), owner, diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/state.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/state.rs index 58f1f6729..cbc6bce52 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/state.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/state.rs @@ -1,4 +1,86 @@ -use andromeda_non_fungible_tokens::crowdfund::CampaignConfig; -use cw_storage_plus::Item; +use andromeda_non_fungible_tokens::crowdfund::{CampaignConfig, CampaignStage, Tier}; +use andromeda_std::error::ContractError; +use cosmwasm_std::{ensure, Storage, Uint128}; +use cw_storage_plus::{Item, Map}; pub const CAMPAIGN_CONFIG: Item = Item::new("campaign_config"); + +pub const CAMPAIGN_STAGE: Item = Item::new("campaign_stage"); + +pub const CURRENT_CAP: Item = Item::new("current_capital"); + +pub const TIERS: Map = Map::new("tiers"); + +pub(crate) fn update_config( + storage: &mut dyn Storage, + config: CampaignConfig, +) -> Result<(), ContractError> { + CAMPAIGN_CONFIG + .save(storage, &config) + .map_err(ContractError::Std) +} + +/// Only used on the instantiation +pub(crate) fn set_tiers(storage: &mut dyn Storage, tiers: Vec) -> Result<(), ContractError> { + for tier in tiers { + ensure!( + !TIERS.has(storage, tier.level.into()), + ContractError::InvalidTier { + operation: "instantiate".to_string(), + msg: format!("Tier with level {} already defined", tier.level) + } + ); + TIERS.save(storage, tier.level.into(), &tier)?; + } + + Ok(()) +} + +pub(crate) fn add_tier(storage: &mut dyn Storage, tier: &Tier) -> Result<(), ContractError> { + ensure!( + !TIERS.has(storage, tier.level.into()), + ContractError::InvalidTier { + operation: "add".to_string(), + msg: format!("Tier with level {} already exist", tier.level) + } + ); + TIERS.save(storage, tier.level.into(), tier)?; + Ok(()) +} + +pub(crate) fn update_tier(storage: &mut dyn Storage, tier: &Tier) -> Result<(), ContractError> { + ensure!( + TIERS.has(storage, tier.level.into()), + ContractError::InvalidTier { + operation: "update".to_string(), + msg: format!("Tier with level {} does not exist", tier.level), + } + ); + + TIERS.save(storage, tier.level.into(), tier)?; + Ok(()) +} + +pub(crate) fn remove_tier(storage: &mut dyn Storage, level: u64) -> Result<(), ContractError> { + ensure!( + TIERS.has(storage, level), + ContractError::InvalidTier { + operation: "remove".to_string(), + msg: format!("Tier with level {} does not exist", level) + } + ); + + TIERS.remove(storage, level); + Ok(()) +} + +// pub(crate) fn validate_tiers(storage: &mut dyn Storage) -> bool { +// !TIERS.is_empty(storage) +// && TIERS +// .range_raw(storage, None, None, Order::Ascending) +// .any(|res| res.unwrap().1.limit.is_none()) +// } + +pub(crate) fn get_current_stage(storage: &dyn Storage) -> CampaignStage { + CAMPAIGN_STAGE.load(storage).unwrap_or(CampaignStage::READY) +} diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs index 614f49f7a..3ab5f379b 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs @@ -1,13 +1,16 @@ -use andromeda_non_fungible_tokens::crowdfund::CampaignConfig; +use andromeda_non_fungible_tokens::{ + crowdfund::{CampaignConfig, Tier, TierMetaData}, + cw721::TokenExtension, +}; use andromeda_std::{ ado_base::InstantiateMsg, ado_contract::ADOContract, amp::AndrAddr, - testing::mock_querier::{WasmMockQuerier, MOCK_KERNEL_CONTRACT}, + testing::mock_querier::{WasmMockQuerier, MOCK_ADO_PUBLISHER, MOCK_KERNEL_CONTRACT}, }; use cosmwasm_std::{ testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, - Coin, OwnedDeps, QuerierWrapper, Uint128, + Coin, OwnedDeps, QuerierWrapper, Uint128, Uint64, }; pub const MOCK_TIER_CONTRACT: &str = "tier_contract"; @@ -27,6 +30,21 @@ pub fn mock_campaign_config() -> CampaignConfig { } } +pub fn mock_campaign_tiers() -> Vec { + vec![Tier { + level: Uint64::zero(), + limit: None, + price: Uint128::new(10u128), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + owner: None, + token_uri: None, + }, + }] +} + /// Alternative to `cosmwasm_std::testing::mock_dependencies` that allows us to respond to custom queries. /// /// Automatically assigns a kernel address as MOCK_KERNEL_CONTRACT. diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs index c7673f4ec..12832d124 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs @@ -9,12 +9,14 @@ use crate::{ contract::instantiate, state::CAMPAIGN_CONFIG, testing::mock_querier::mock_dependencies_custom, }; -use super::mock_querier::mock_campaign_config; +use super::mock_querier::{mock_campaign_config, mock_campaign_tiers}; fn init(deps: DepsMut, modules: Option>) -> Response { let config = mock_campaign_config(); + let tiers = mock_campaign_tiers(); let msg = InstantiateMsg { campaign_config: config, + tiers, owner: None, modules, kernel_address: MOCK_KERNEL_CONTRACT.to_string(), diff --git a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs index c92ac2ba8..1ecedba98 100644 --- a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs +++ b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs @@ -3,7 +3,9 @@ use andromeda_std::common::denom::validate_denom; use andromeda_std::error::ContractError; use andromeda_std::{andr_exec, andr_instantiate, andr_instantiate_modules, andr_query}; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{ensure, Deps, Uint128}; +use cosmwasm_std::{ensure, Deps, Uint128, Uint64}; + +use crate::cw721::TokenExtension; #[andr_instantiate] #[andr_instantiate_modules] @@ -11,11 +13,23 @@ use cosmwasm_std::{ensure, Deps, Uint128}; pub struct InstantiateMsg { /// The configuration for the campaign pub campaign_config: CampaignConfig, + /// The tiers for the campaign + pub tiers: Vec, } #[andr_exec] #[cw_serde] -pub enum ExecuteMsg {} +pub enum ExecuteMsg { + /// Add a tier + AddTier { tier: Tier }, + /// Update an existing tier + UpdateTier { tier: Tier }, + /// Remove a tier + RemoveTier { level: Uint64 }, + + /// Start the campaign + StartCampaign {}, +} #[andr_query] #[cw_serde] @@ -87,3 +101,47 @@ pub enum CampaignStage { /// Stage when campaign failed to meet the target cap before expiration FAILED, } + +impl ToString for CampaignStage { + #[inline] + fn to_string(&self) -> String { + match self { + Self::READY => "READY".to_string(), + Self::ONGOING => "ONGOING".to_string(), + Self::SUCCEED => "SUCCEED".to_string(), + Self::FAILED => "FAILED".to_string(), + } + } +} + +#[cw_serde] +pub struct Tier { + pub level: Uint64, + pub price: Uint128, + pub limit: Option, // None for no limit + pub meta_data: TierMetaData, +} + +impl Tier { + pub fn validate(&self) -> Result<(), ContractError> { + ensure!( + !self.price.is_zero(), + ContractError::InvalidTier { + operation: "all".to_string(), + msg: "Price can not be zero".to_string() + } + ); + Ok(()) + } +} +#[cw_serde] +pub struct TierMetaData { + /// The owner of the tier + pub owner: Option, + /// Universal resource identifier for the tier + /// Should point to a JSON file that conforms to the ERC721 + /// Metadata JSON Schema + pub token_uri: Option, + /// Any custom extension used by this contract + pub extension: TokenExtension, +} diff --git a/packages/std/src/error.rs b/packages/std/src/error.rs index 478570a2c..e0203c027 100644 --- a/packages/std/src/error.rs +++ b/packages/std/src/error.rs @@ -51,6 +51,8 @@ pub enum ContractError { operation: String, validator: String, }, + #[error("Invalid Campaign Operation: {operation} on {stage}")] + InvalidCampaignOperation { operation: String, stage: String }, #[error("No Staking Reward")] InvalidClaim {}, @@ -659,6 +661,12 @@ pub enum ContractError { #[error("Invalid time: {msg}")] InvalidTimestamp { msg: String }, + + #[error("At least one tier should have no limit")] + InvalidTiers {}, + + #[error("Invalid tier for {operation} operation: {msg} ")] + InvalidTier { operation: String, msg: String }, } impl From for ContractError { From 4b0dfb9a25613c226a43fa5a7fcbcec9749f07a8 Mon Sep 17 00:00:00 2001 From: cowboy0015 Date: Fri, 3 May 2024 10:29:13 -0400 Subject: [PATCH 03/11] fix: added tier management unit tests --- .../andromeda-crowdfund/src/contract.rs | 4 +- .../andromeda-crowdfund/src/state.rs | 1 + .../andromeda-crowdfund/src/testing/tests.rs | 402 +++++++++++++++++- 3 files changed, 402 insertions(+), 5 deletions(-) diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs index 758a96d5e..1e1c12593 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs @@ -217,7 +217,9 @@ fn execute_remove_tier(ctx: ExecuteContext, level: Uint64) -> Result) -> Result<(), ContractError> { for tier in tiers { + tier.validate()?; ensure!( !TIERS.has(storage, tier.level.into()), ContractError::InvalidTier { diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs index 12832d124..d14581dcb 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs @@ -1,12 +1,23 @@ -use andromeda_non_fungible_tokens::crowdfund::InstantiateMsg; -use andromeda_std::{ado_base::Module, testing::mock_querier::MOCK_KERNEL_CONTRACT}; +use andromeda_non_fungible_tokens::{ + crowdfund::{ExecuteMsg, InstantiateMsg, Tier, TierMetaData}, + cw721::TokenExtension, +}; +use andromeda_std::{ + ado_base::Module, + common::reply::ReplyId, + error::ContractError, + os::economics::ExecuteMsg as EconomicsExecuteMsg, + testing::mock_querier::{MOCK_ADO_PUBLISHER, MOCK_KERNEL_CONTRACT}, +}; use cosmwasm_std::{ testing::{mock_env, mock_info}, - DepsMut, Response, + to_json_binary, Addr, CosmosMsg, DepsMut, Response, SubMsg, Uint128, Uint64, WasmMsg, }; use crate::{ - contract::instantiate, state::CAMPAIGN_CONFIG, testing::mock_querier::mock_dependencies_custom, + contract::{execute, instantiate}, + state::CAMPAIGN_CONFIG, + testing::mock_querier::mock_dependencies_custom, }; use super::mock_querier::{mock_campaign_config, mock_campaign_tiers}; @@ -47,6 +58,389 @@ fn test_instantiate() { ); } +#[test] +fn test_instantiate_invalid_tiers() { + let mut deps = mock_dependencies_custom(&[]); + let config = mock_campaign_config(); + let mut tiers = mock_campaign_tiers(); + tiers.push(Tier { + level: Uint64::new(1u64), + limit: Some(Uint128::new(100)), + price: Uint128::zero(), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + owner: None, + token_uri: None, + }, + }); + let msg = InstantiateMsg { + campaign_config: config, + tiers, + owner: None, + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + modules: None, + }; + + let info = mock_info("owner", &[]); + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + + assert_eq!( + res, + ContractError::InvalidTier { + operation: "all".to_string(), + msg: "Price can not be zero".to_string() + } + ); + + assert_eq!( + mock_campaign_config(), + CAMPAIGN_CONFIG.load(deps.as_mut().storage).unwrap() + ); +} + +#[test] +fn test_add_tier() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let msg = ExecuteMsg::AddTier { + tier: Tier { + level: Uint64::new(1u64), + limit: Some(Uint128::new(100)), + price: Uint128::new(100), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + owner: None, + token_uri: None, + }, + }, + }; + + let info = mock_info("owner", &[]); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!( + res, + Response::new() + .add_attribute("action", "add_tier") + .add_attribute("level", "1") + .add_attribute("price", "100") + .add_attribute("limit", "100") + // Economics message + .add_submessage(SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "economics_contract".to_string(), + msg: to_json_binary(&EconomicsExecuteMsg::PayFee { + payee: Addr::unchecked("owner"), + action: "AddTier".to_string() + }) + .unwrap(), + funds: vec![], + }), + ReplyId::PayFee.repr(), + )), + ); +} + +#[test] +fn test_add_tier_unauthorized() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let msg = ExecuteMsg::AddTier { + tier: Tier { + level: Uint64::new(1u64), + limit: Some(Uint128::new(100)), + price: Uint128::new(100), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + owner: None, + token_uri: None, + }, + }, + }; + + let info = mock_info("owner1", &[]); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); +} + +#[test] +fn test_add_tier_zero_price() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let msg = ExecuteMsg::AddTier { + tier: Tier { + level: Uint64::new(1u64), + limit: Some(Uint128::new(100)), + price: Uint128::zero(), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + owner: None, + token_uri: None, + }, + }, + }; + + let info = mock_info("owner", &[]); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidTier { + operation: "all".to_string(), + msg: "Price can not be zero".to_string() + } + ); +} + +#[test] +fn test_add_tier_duplicated() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let msg = ExecuteMsg::AddTier { + tier: Tier { + level: Uint64::zero(), + limit: Some(Uint128::new(100u128)), + price: Uint128::new(100u128), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + owner: None, + token_uri: None, + }, + }, + }; + + let info = mock_info("owner", &[]); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidTier { + operation: "add".to_string(), + msg: "Tier with level 0 already exist".to_string() + } + ); +} + +#[test] +fn test_update_tier() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let msg = ExecuteMsg::UpdateTier { + tier: Tier { + level: Uint64::zero(), + limit: Some(Uint128::new(100)), + price: Uint128::new(100), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + owner: None, + token_uri: None, + }, + }, + }; + + let info = mock_info("owner", &[]); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!( + res, + Response::new() + .add_attribute("action", "update_tier") + .add_attribute("level", "0") + .add_attribute("price", "100") + .add_attribute("limit", "100") + // Economics message + .add_submessage(SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "economics_contract".to_string(), + msg: to_json_binary(&EconomicsExecuteMsg::PayFee { + payee: Addr::unchecked("owner"), + action: "UpdateTier".to_string() + }) + .unwrap(), + funds: vec![], + }), + ReplyId::PayFee.repr(), + )), + ); +} + +#[test] +fn test_update_tier_unauthorized() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let msg = ExecuteMsg::UpdateTier { + tier: Tier { + level: Uint64::zero(), + limit: Some(Uint128::new(100)), + price: Uint128::new(100), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + owner: None, + token_uri: None, + }, + }, + }; + + let info = mock_info("owner1", &[]); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); +} + +#[test] +fn test_update_tier_zero_price() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let msg = ExecuteMsg::UpdateTier { + tier: Tier { + level: Uint64::zero(), + limit: Some(Uint128::new(100)), + price: Uint128::zero(), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + owner: None, + token_uri: None, + }, + }, + }; + + let info = mock_info("owner", &[]); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidTier { + operation: "all".to_string(), + msg: "Price can not be zero".to_string() + } + ); +} + +#[test] +fn test_update_tier_non_exist() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let msg = ExecuteMsg::UpdateTier { + tier: Tier { + level: Uint64::new(1u64), + limit: Some(Uint128::new(100u128)), + price: Uint128::new(100u128), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + owner: None, + token_uri: None, + }, + }, + }; + + let info = mock_info("owner", &[]); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidTier { + operation: "update".to_string(), + msg: "Tier with level 1 does not exist".to_string() + } + ); +} + +#[test] +fn test_remove_tier() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let msg = ExecuteMsg::RemoveTier { + level: Uint64::zero(), + }; + + let info = mock_info("owner", &[]); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!( + res, + Response::new() + .add_attribute("action", "remove_tier") + .add_attribute("level", "0") + // Economics message + .add_submessage(SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "economics_contract".to_string(), + msg: to_json_binary(&EconomicsExecuteMsg::PayFee { + payee: Addr::unchecked("owner"), + action: "RemoveTier".to_string() + }) + .unwrap(), + funds: vec![], + }), + ReplyId::PayFee.repr(), + )), + ); +} + +#[test] +fn test_remvoe_tier_unauthorized() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let msg = ExecuteMsg::RemoveTier { + level: Uint64::zero(), + }; + + let info = mock_info("owner1", &[]); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); +} + +#[test] +fn test_remove_tier_non_exist() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let msg = ExecuteMsg::RemoveTier { + level: Uint64::new(1u64), + }; + + let info = mock_info("owner", &[]); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidTier { + operation: "remove".to_string(), + msg: "Tier with level 1 does not exist".to_string() + } + ); +} + // #[test] // fn test_mint_unauthorized() { // let mut deps = mock_dependencies_custom(&[]); From 725f1c892c0f249c3ed242cadf17a59ff7d6ca48 Mon Sep 17 00:00:00 2001 From: cowboy0015 Date: Mon, 6 May 2024 03:22:04 -0400 Subject: [PATCH 04/11] fix: adjusted campaign config title and description validation --- .../andromeda-non-fungible-tokens/src/crowdfund.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs index 1ecedba98..984796112 100644 --- a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs +++ b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs @@ -38,9 +38,9 @@ pub enum QueryMsg {} #[cw_serde] pub struct CampaignConfig { - /// Title of the campaign. Maximum length is 32. + /// Title of the campaign. Maximum length is 64. pub title: String, - /// Short description about the campaign. Maximum length is 256. + /// Short description about the campaign. pub description: String, /// URL for the banner of the campaign pub banner: String, @@ -67,15 +67,9 @@ impl CampaignConfig { // validate meta info ensure!( - self.title.len() <= 32, + self.title.len() <= 64, ContractError::InvalidParameter { - error: Some("Title length can be 32 at maximum".to_string()) - } - ); - ensure!( - self.description.len() <= 256, - ContractError::InvalidParameter { - error: Some("Description length can be 256 at maximum".to_string()) + error: Some("Title length can be 64 at maximum".to_string()) } ); From 72af22d85b4aa21d07ddb9bfa3aa93aff730c7ad Mon Sep 17 00:00:00 2001 From: cowboy0015 Date: Mon, 6 May 2024 03:45:45 -0400 Subject: [PATCH 05/11] fix:updated hard_cap as optional --- .../andromeda-crowdfund/src/testing/mock_querier.rs | 2 +- .../andromeda-non-fungible-tokens/src/crowdfund.rs | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs index 3ab5f379b..2ec48d810 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs @@ -26,7 +26,7 @@ pub fn mock_campaign_config() -> CampaignConfig { tier_address: AndrAddr::from_string(MOCK_TIER_CONTRACT.to_owned()), withdrawal_address: AndrAddr::from_string(MOCK_WITHDRAWAL_ADDRESS.to_owned()), soft_cap: None, - hard_cap: Uint128::from(5000u128), + hard_cap: None, } } diff --git a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs index 984796112..0f5f2c208 100644 --- a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs +++ b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs @@ -46,16 +46,16 @@ pub struct CampaignConfig { pub banner: String, /// Official website of the campaign pub url: String, - /// Withdrawal address for the funds gained by the campaign - pub tier_address: AndrAddr, /// The address of the tier contract whose tokens are being distributed + pub tier_address: AndrAddr, + /// The denom of the token that is being accepted by the campaign pub denom: String, - /// The minimum amount of funding to be sold for the successful fundraising + /// Withdrawal address for the funds gained by the campaign pub withdrawal_address: AndrAddr, - /// The address of the tier contract whose tokens are being distributed + /// The minimum amount of funding to be sold for the successful fundraising pub soft_cap: Option, /// The maximum amount of funding to be sold for the fundraising - pub hard_cap: Uint128, + pub hard_cap: Option, } impl CampaignConfig { @@ -75,7 +75,8 @@ impl CampaignConfig { // validate target capital ensure!( - (self.soft_cap).map_or(true, |soft_cap| soft_cap < self.hard_cap), + (self.soft_cap).map_or(true, |soft_cap| soft_cap + < self.hard_cap.unwrap_or(soft_cap + Uint128::new(1))), ContractError::InvalidParameter { error: Some("soft_cap can not exceed hard_cap".to_string()) } From b5624dde75dde80f77687ceadc950467bc8d4344 Mon Sep 17 00:00:00 2001 From: cowboy0015 Date: Tue, 7 May 2024 12:07:03 -0400 Subject: [PATCH 06/11] feat: added start campaign implementation, cleaned up the test cases according to the review --- .../andromeda-crowdfund/src/contract.rs | 66 ++++- .../andromeda-crowdfund/src/state.rs | 61 +++- .../src/testing/mock_querier.rs | 39 ++- .../andromeda-crowdfund/src/testing/tests.rs | 278 +++++++++++++++--- .../src/crowdfund.rs | 35 ++- 5 files changed, 407 insertions(+), 72 deletions(-) diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs index 1e1c12593..bad349c2f 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs @@ -3,8 +3,9 @@ // PURCHASES, SALE_CONDUCTED, STATE, // }; use andromeda_non_fungible_tokens::crowdfund::{ - CampaignStage, ExecuteMsg, InstantiateMsg, QueryMsg, Tier, + CampaignStage, ExecuteMsg, InstantiateMsg, QueryMsg, Tier, TierOrder, }; +use andromeda_std::common::{Milliseconds, MillisecondsExpiration}; use andromeda_std::{ado_base::ownership::OwnershipMessage, common::actions::call_action}; use andromeda_std::{ado_contract::ADOContract, common::context::ExecuteContext}; @@ -21,7 +22,8 @@ use cosmwasm_std::{ }; use crate::state::{ - add_tier, get_current_stage, remove_tier, set_tiers, update_config, update_tier, + add_tier, get_config, get_current_stage, is_valid_tiers, remove_tier, set_current_stage, + set_tier_orders, set_tiers, update_config, update_tier, }; const CONTRACT_NAME: &str = "crates.io:andromeda-crowdfund"; @@ -120,6 +122,11 @@ pub fn handle_execute(mut ctx: ExecuteContext, msg: ExecuteMsg) -> Result execute_add_tier(ctx, tier), ExecuteMsg::UpdateTier { tier } => execute_update_tier(ctx, tier), ExecuteMsg::RemoveTier { level } => execute_remove_tier(ctx, level), + ExecuteMsg::StartCampaign { + start_time, + end_time, + presale, + } => execute_start_campaign(ctx, start_time, end_time, presale), _ => ADOContract::default().execute(ctx, msg), }?; @@ -224,6 +231,61 @@ fn execute_remove_tier(ctx: ExecuteContext, level: Uint64) -> Result, + end_time: MillisecondsExpiration, + presale: Option>, +) -> Result { + let ExecuteContext { + deps, info, env, .. + } = ctx; + + // Only owner can start the campaign + let contract = ADOContract::default(); + ensure!( + contract.is_contract_owner(deps.storage, info.sender.as_str())?, + ContractError::Unauthorized {} + ); + + // At least one tier should have no limit to start the campaign + ensure!(is_valid_tiers(deps.storage), ContractError::InvalidTiers {}); + + // Validate parameters + ensure!( + !end_time.is_expired(&env.block) && start_time.unwrap_or(Milliseconds::zero()) < end_time, + ContractError::StartTimeAfterEndTime {} + ); + + // Campaign can only start on READY stage + let curr_stage = get_current_stage(deps.storage); + ensure!( + curr_stage == CampaignStage::READY, + ContractError::InvalidCampaignOperation { + operation: "start_campaign".to_string(), + stage: curr_stage.to_string() + } + ); + + // Update tier limit and update sender's order based on presale + if let Some(presale) = presale { + set_tier_orders(deps.storage, presale)?; + } + + // Set start time and end time + let mut config = get_config(deps.storage)?; + config.start_time = start_time; + config.end_time = end_time; + update_config(deps.storage, config)?; + + // update stage + set_current_stage(deps.storage, CampaignStage::ONGOING)?; + + let resp = Response::new().add_attribute("action", "start_campaign"); + + Ok(resp) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { ADOContract::default().query(deps, env, msg) diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/state.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/state.rs index 007b1a1e3..0e0cf8dea 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/state.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/state.rs @@ -1,6 +1,6 @@ -use andromeda_non_fungible_tokens::crowdfund::{CampaignConfig, CampaignStage, Tier}; +use andromeda_non_fungible_tokens::crowdfund::{CampaignConfig, CampaignStage, Tier, TierOrder}; use andromeda_std::error::ContractError; -use cosmwasm_std::{ensure, Storage, Uint128}; +use cosmwasm_std::{ensure, Addr, Order, Storage, Uint128}; use cw_storage_plus::{Item, Map}; pub const CAMPAIGN_CONFIG: Item = Item::new("campaign_config"); @@ -11,6 +11,8 @@ pub const CURRENT_CAP: Item = Item::new("current_capital"); pub const TIERS: Map = Map::new("tiers"); +pub const TIER_ORDERS: Map<(Addr, u64), u128> = Map::new("tier_orders"); + pub(crate) fn update_config( storage: &mut dyn Storage, config: CampaignConfig, @@ -20,6 +22,10 @@ pub(crate) fn update_config( .map_err(ContractError::Std) } +pub(crate) fn get_config(storage: &dyn Storage) -> Result { + CAMPAIGN_CONFIG.load(storage).map_err(ContractError::Std) +} + /// Only used on the instantiation pub(crate) fn set_tiers(storage: &mut dyn Storage, tiers: Vec) -> Result<(), ContractError> { for tier in tiers { @@ -75,13 +81,52 @@ pub(crate) fn remove_tier(storage: &mut dyn Storage, level: u64) -> Result<(), C Ok(()) } -// pub(crate) fn validate_tiers(storage: &mut dyn Storage) -> bool { -// !TIERS.is_empty(storage) -// && TIERS -// .range_raw(storage, None, None, Order::Ascending) -// .any(|res| res.unwrap().1.limit.is_none()) -// } +pub(crate) fn is_valid_tiers(storage: &mut dyn Storage) -> bool { + !TIERS.is_empty(storage) + && TIERS + .range_raw(storage, None, None, Order::Ascending) + .any(|res| res.unwrap().1.limit.is_none()) +} pub(crate) fn get_current_stage(storage: &dyn Storage) -> CampaignStage { CAMPAIGN_STAGE.load(storage).unwrap_or(CampaignStage::READY) } + +pub(crate) fn set_current_stage( + storage: &mut dyn Storage, + stage: CampaignStage, +) -> Result<(), ContractError> { + CAMPAIGN_STAGE + .save(storage, &stage) + .map_err(ContractError::Std) +} + +pub(crate) fn set_tier_orders( + storage: &mut dyn Storage, + orders: Vec, +) -> Result<(), ContractError> { + for new_order in orders { + let mut tier = TIERS.load(storage, new_order.level.into()).map_err(|_| { + ContractError::InvalidTier { + operation: "set_tier_orders".to_string(), + msg: format!("Tier with level {} does not exist", new_order.level), + } + })?; + if let Some(mut remaining_amount) = tier.limit { + remaining_amount = remaining_amount.checked_sub(new_order.amount)?; + tier.limit = Some(remaining_amount); + update_tier(storage, &tier)?; + } + + let mut order = TIER_ORDERS + .load(storage, (new_order.orderer.clone(), new_order.level.into())) + .unwrap_or(0); + order += new_order.amount.u128(); + TIER_ORDERS.save( + storage, + (new_order.orderer.clone(), new_order.level.into()), + &order, + )?; + } + Ok(()) +} diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs index 2ec48d810..b31f0142e 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs @@ -5,7 +5,8 @@ use andromeda_non_fungible_tokens::{ use andromeda_std::{ ado_base::InstantiateMsg, ado_contract::ADOContract, - amp::AndrAddr, + amp::{AndrAddr, Recipient}, + common::MillisecondsExpiration, testing::mock_querier::{WasmMockQuerier, MOCK_ADO_PUBLISHER, MOCK_KERNEL_CONTRACT}, }; use cosmwasm_std::{ @@ -24,25 +25,39 @@ pub fn mock_campaign_config() -> CampaignConfig { url: "http://".to_string(), denom: "uandr".to_string(), tier_address: AndrAddr::from_string(MOCK_TIER_CONTRACT.to_owned()), - withdrawal_address: AndrAddr::from_string(MOCK_WITHDRAWAL_ADDRESS.to_owned()), + withdrawal_recipient: Recipient::from_string(MOCK_WITHDRAWAL_ADDRESS.to_owned()), soft_cap: None, hard_cap: None, + start_time: None, + end_time: MillisecondsExpiration::zero(), } } pub fn mock_campaign_tiers() -> Vec { - vec![Tier { - level: Uint64::zero(), - limit: None, - price: Uint128::new(10u128), - meta_data: TierMetaData { - extension: TokenExtension { - publisher: MOCK_ADO_PUBLISHER.to_string(), + vec![ + Tier { + level: Uint64::zero(), + limit: None, + price: Uint128::new(10u128), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + token_uri: None, }, - owner: None, - token_uri: None, }, - }] + Tier { + level: Uint64::new(1u64), + limit: Some(Uint128::new(1000u128)), + price: Uint128::new(10u128), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + token_uri: None, + }, + }, + ] } /// Alternative to `cosmwasm_std::testing::mock_dependencies` that allows us to respond to custom queries. diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs index d14581dcb..aeebd7668 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs @@ -1,22 +1,22 @@ use andromeda_non_fungible_tokens::{ - crowdfund::{ExecuteMsg, InstantiateMsg, Tier, TierMetaData}, + crowdfund::{ExecuteMsg, InstantiateMsg, Tier, TierMetaData, TierOrder}, cw721::TokenExtension, }; use andromeda_std::{ ado_base::Module, - common::reply::ReplyId, + common::{reply::ReplyId, MillisecondsExpiration}, error::ContractError, os::economics::ExecuteMsg as EconomicsExecuteMsg, testing::mock_querier::{MOCK_ADO_PUBLISHER, MOCK_KERNEL_CONTRACT}, }; use cosmwasm_std::{ testing::{mock_env, mock_info}, - to_json_binary, Addr, CosmosMsg, DepsMut, Response, SubMsg, Uint128, Uint64, WasmMsg, + to_json_binary, Addr, CosmosMsg, DepsMut, Order, Response, SubMsg, Uint128, Uint64, WasmMsg, }; use crate::{ contract::{execute, instantiate}, - state::CAMPAIGN_CONFIG, + state::{CAMPAIGN_CONFIG, TIERS, TIER_ORDERS}, testing::mock_querier::mock_dependencies_custom, }; @@ -56,6 +56,12 @@ fn test_instantiate() { mock_campaign_config(), CAMPAIGN_CONFIG.load(deps.as_mut().storage).unwrap() ); + + let tiers: Vec = TIERS + .range_raw(deps.as_ref().storage, None, None, Order::Ascending) + .map(|res| res.unwrap().1) + .collect(); + assert_eq!(mock_campaign_tiers(), tiers); } #[test] @@ -71,7 +77,6 @@ fn test_instantiate_invalid_tiers() { extension: TokenExtension { publisher: MOCK_ADO_PUBLISHER.to_string(), }, - owner: None, token_uri: None, }, }); @@ -106,28 +111,32 @@ fn test_add_tier() { let _ = init(deps.as_mut(), None); - let msg = ExecuteMsg::AddTier { - tier: Tier { - level: Uint64::new(1u64), - limit: Some(Uint128::new(100)), - price: Uint128::new(100), - meta_data: TierMetaData { - extension: TokenExtension { - publisher: MOCK_ADO_PUBLISHER.to_string(), - }, - owner: None, - token_uri: None, + let tier_to_add = Tier { + level: Uint64::new(2u64), + limit: Some(Uint128::new(100)), + price: Uint128::new(100), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), }, + token_uri: None, }, }; + // Tier with the same level does not exist before adding. + assert!(!TIERS.has(deps.as_ref().storage, tier_to_add.level.into())); + + let msg = ExecuteMsg::AddTier { + tier: tier_to_add.clone(), + }; + let info = mock_info("owner", &[]); let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); assert_eq!( res, Response::new() .add_attribute("action", "add_tier") - .add_attribute("level", "1") + .add_attribute("level", "2") .add_attribute("price", "100") .add_attribute("limit", "100") // Economics message @@ -144,6 +153,14 @@ fn test_add_tier() { ReplyId::PayFee.repr(), )), ); + + // Tier is saved in the state. + assert_eq!( + tier_to_add, + TIERS + .load(deps.as_ref().storage, tier_to_add.level.into()) + .unwrap() + ); } #[test] @@ -161,7 +178,6 @@ fn test_add_tier_unauthorized() { extension: TokenExtension { publisher: MOCK_ADO_PUBLISHER.to_string(), }, - owner: None, token_uri: None, }, }, @@ -187,7 +203,6 @@ fn test_add_tier_zero_price() { extension: TokenExtension { publisher: MOCK_ADO_PUBLISHER.to_string(), }, - owner: None, token_uri: None, }, }, @@ -219,7 +234,6 @@ fn test_add_tier_duplicated() { extension: TokenExtension { publisher: MOCK_ADO_PUBLISHER.to_string(), }, - owner: None, token_uri: None, }, }, @@ -242,21 +256,30 @@ fn test_update_tier() { let _ = init(deps.as_mut(), None); - let msg = ExecuteMsg::UpdateTier { - tier: Tier { - level: Uint64::zero(), - limit: Some(Uint128::new(100)), - price: Uint128::new(100), - meta_data: TierMetaData { - extension: TokenExtension { - publisher: MOCK_ADO_PUBLISHER.to_string(), - }, - owner: None, - token_uri: None, + let updated_tier = Tier { + level: Uint64::zero(), + limit: Some(Uint128::new(100)), + price: Uint128::new(100), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), }, + token_uri: None, }, }; + // Before updating, tier with same level exists but their data is different. + assert_ne!( + updated_tier, + TIERS + .load(deps.as_ref().storage, updated_tier.level.into()) + .unwrap() + ); + + let msg = ExecuteMsg::UpdateTier { + tier: updated_tier.clone(), + }; + let info = mock_info("owner", &[]); let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); assert_eq!( @@ -280,6 +303,14 @@ fn test_update_tier() { ReplyId::PayFee.repr(), )), ); + + // Tier updated successfully. + assert_eq!( + updated_tier, + TIERS + .load(deps.as_ref().storage, updated_tier.level.into()) + .unwrap() + ); } #[test] @@ -297,7 +328,6 @@ fn test_update_tier_unauthorized() { extension: TokenExtension { publisher: MOCK_ADO_PUBLISHER.to_string(), }, - owner: None, token_uri: None, }, }, @@ -323,7 +353,6 @@ fn test_update_tier_zero_price() { extension: TokenExtension { publisher: MOCK_ADO_PUBLISHER.to_string(), }, - owner: None, token_uri: None, }, }, @@ -348,14 +377,13 @@ fn test_update_tier_non_exist() { let msg = ExecuteMsg::UpdateTier { tier: Tier { - level: Uint64::new(1u64), + level: Uint64::new(2u64), limit: Some(Uint128::new(100u128)), price: Uint128::new(100u128), meta_data: TierMetaData { extension: TokenExtension { publisher: MOCK_ADO_PUBLISHER.to_string(), }, - owner: None, token_uri: None, }, }, @@ -367,7 +395,7 @@ fn test_update_tier_non_exist() { err, ContractError::InvalidTier { operation: "update".to_string(), - msg: "Tier with level 1 does not exist".to_string() + msg: "Tier with level 2 does not exist".to_string() } ); } @@ -378,8 +406,13 @@ fn test_remove_tier() { let _ = init(deps.as_mut(), None); + let level_to_remove = Uint64::zero(); + + // The tier to be removed exists before removing + assert!(TIERS.has(deps.as_ref().storage, level_to_remove.into())); + let msg = ExecuteMsg::RemoveTier { - level: Uint64::zero(), + level: level_to_remove, }; let info = mock_info("owner", &[]); @@ -403,10 +436,13 @@ fn test_remove_tier() { ReplyId::PayFee.repr(), )), ); + + // The tier removed successfully + assert!(!TIERS.has(deps.as_ref().storage, level_to_remove.into())); } #[test] -fn test_remvoe_tier_unauthorized() { +fn test_remove_tier_unauthorized() { let mut deps = mock_dependencies_custom(&[]); let _ = init(deps.as_mut(), None); @@ -427,7 +463,7 @@ fn test_remove_tier_non_exist() { let _ = init(deps.as_mut(), None); let msg = ExecuteMsg::RemoveTier { - level: Uint64::new(1u64), + level: Uint64::new(2u64), }; let info = mock_info("owner", &[]); @@ -436,11 +472,173 @@ fn test_remove_tier_non_exist() { err, ContractError::InvalidTier { operation: "remove".to_string(), - msg: "Tier with level 1 does not exist".to_string() + msg: "Tier with level 2 does not exist".to_string() } ); } +#[test] +fn test_start_campaign() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let env = mock_env(); + let initial_limit = TIERS.load(&deps.storage, 1).unwrap().limit.unwrap(); + + let mock_orderer = Addr::unchecked("mock_orderer".to_string()); + let msg = ExecuteMsg::StartCampaign { + start_time: None, + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 100), + presale: Some(vec![TierOrder { + amount: Uint128::new(100u128), + level: Uint64::new(1u64), + orderer: mock_orderer.clone(), + }]), + }; + + let info = mock_info("owner", &[]); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + + let limit = TIERS.load(&deps.storage, 1).unwrap().limit.unwrap(); + + assert_eq!( + res, + Response::new() + .add_attribute("action", "start_campaign") + .add_submessage(SubMsg::reply_on_error( + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "economics_contract".to_string(), + msg: to_json_binary(&EconomicsExecuteMsg::PayFee { + payee: Addr::unchecked("owner"), + action: "StartCampaign".to_string() + }) + .unwrap(), + funds: vec![], + }), + ReplyId::PayFee.repr(), + )), + ); + assert_eq!(initial_limit, limit + Uint128::new(100)); + + let order = TIER_ORDERS.load(&deps.storage, (mock_orderer, 1)).unwrap(); + assert_eq!(order, 100); +} + +#[test] +fn test_start_campaign_unauthorized() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let env = mock_env(); + + let msg = ExecuteMsg::StartCampaign { + start_time: None, + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 100), + presale: None, + }; + + let info = mock_info("owner1", &[]); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + + assert_eq!(err, ContractError::Unauthorized {}); +} + +#[test] +fn test_start_campaign_invalid_tiers() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + // Remove level 0 tier that has no limit + TIERS.remove(&mut deps.storage, 0); + let env = mock_env(); + + let msg = ExecuteMsg::StartCampaign { + start_time: None, + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 100), + presale: None, + }; + + let info = mock_info("owner", &[]); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + + assert_eq!(err, ContractError::InvalidTiers {}); +} + +#[test] +fn test_start_campaign_invalid_presale() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let env = mock_env(); + + let msg = ExecuteMsg::StartCampaign { + start_time: None, + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 100), + presale: Some(vec![TierOrder { + amount: Uint128::new(100u128), + level: Uint64::new(2u64), + orderer: Addr::unchecked("mock_orderer"), + }]), + }; + + let info = mock_info("owner", &[]); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + + assert_eq!( + err, + ContractError::InvalidTier { + operation: "set_tier_orders".to_string(), + msg: "Tier with level 2 does not exist".to_string() + } + ); +} + +#[test] +fn test_start_campaign_invalid_end_time() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let env = mock_env(); + + let msg = ExecuteMsg::StartCampaign { + start_time: None, + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() - 100), + presale: None, + }; + + let info = mock_info("owner", &[]); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + + assert_eq!(err, ContractError::StartTimeAfterEndTime {}); +} + +#[test] +fn test_start_campaign_invalid_start_time() { + let mut deps = mock_dependencies_custom(&[]); + + let _ = init(deps.as_mut(), None); + + let env = mock_env(); + + let msg = ExecuteMsg::StartCampaign { + start_time: Some(MillisecondsExpiration::from_seconds( + env.block.time.seconds() + 1000, + )), + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 500), + presale: None, + }; + + let info = mock_info("owner", &[]); + let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + + assert_eq!(err, ContractError::StartTimeAfterEndTime {}); +} + // #[test] // fn test_mint_unauthorized() { // let mut deps = mock_dependencies_custom(&[]); diff --git a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs index 0f5f2c208..5415f5d2d 100644 --- a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs +++ b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs @@ -1,9 +1,11 @@ use andromeda_std::amp::addresses::AndrAddr; +use andromeda_std::amp::Recipient; use andromeda_std::common::denom::validate_denom; +use andromeda_std::common::MillisecondsExpiration; use andromeda_std::error::ContractError; use andromeda_std::{andr_exec, andr_instantiate, andr_instantiate_modules, andr_query}; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{ensure, Deps, Uint128, Uint64}; +use cosmwasm_std::{ensure, Addr, Deps, Uint128, Uint64}; use crate::cw721::TokenExtension; @@ -26,9 +28,12 @@ pub enum ExecuteMsg { UpdateTier { tier: Tier }, /// Remove a tier RemoveTier { level: Uint64 }, - /// Start the campaign - StartCampaign {}, + StartCampaign { + start_time: Option, + end_time: MillisecondsExpiration, + presale: Option>, + }, } #[andr_query] @@ -50,19 +55,23 @@ pub struct CampaignConfig { pub tier_address: AndrAddr, /// The denom of the token that is being accepted by the campaign pub denom: String, - /// Withdrawal address for the funds gained by the campaign - pub withdrawal_address: AndrAddr, + /// Recipient that is upposed to receive the funds gained by the campaign + pub withdrawal_recipient: Recipient, /// The minimum amount of funding to be sold for the successful fundraising pub soft_cap: Option, /// The maximum amount of funding to be sold for the fundraising pub hard_cap: Option, + /// Time when campaign starts + pub start_time: Option, + /// Time when campaign ends + pub end_time: MillisecondsExpiration, } impl CampaignConfig { pub fn validate(&self, deps: Deps) -> Result<(), ContractError> { // validate addresses self.tier_address.validate(deps.api)?; - self.withdrawal_address.validate(deps.api)?; + self.withdrawal_recipient.validate(&deps)?; validate_denom(deps, self.denom.clone())?; // validate meta info @@ -92,7 +101,7 @@ pub enum CampaignStage { /// Stage when campaign is being carried out ONGOING, /// Stage when campaign is finished successfully - SUCCEED, + SUCCESS, /// Stage when campaign failed to meet the target cap before expiration FAILED, } @@ -103,7 +112,7 @@ impl ToString for CampaignStage { match self { Self::READY => "READY".to_string(), Self::ONGOING => "ONGOING".to_string(), - Self::SUCCEED => "SUCCEED".to_string(), + Self::SUCCESS => "SUCCESS".to_string(), Self::FAILED => "FAILED".to_string(), } } @@ -111,12 +120,20 @@ impl ToString for CampaignStage { #[cw_serde] pub struct Tier { + // TODO change to use string pub level: Uint64, pub price: Uint128, pub limit: Option, // None for no limit pub meta_data: TierMetaData, } +#[cw_serde] +pub struct TierOrder { + pub orderer: Addr, + pub level: Uint64, + pub amount: Uint128, +} + impl Tier { pub fn validate(&self) -> Result<(), ContractError> { ensure!( @@ -131,8 +148,6 @@ impl Tier { } #[cw_serde] pub struct TierMetaData { - /// The owner of the tier - pub owner: Option, /// Universal resource identifier for the tier /// Should point to a JSON file that conforms to the ERC721 /// Metadata JSON Schema From 58cdc67e7c375423cafd19ebf0f3b2bac6355083 Mon Sep 17 00:00:00 2001 From: cowboy0015 Date: Tue, 7 May 2024 12:28:47 -0400 Subject: [PATCH 07/11] fix: added additional label field to Tier --- .../andromeda-crowdfund/src/contract.rs | 2 ++ .../src/testing/mock_querier.rs | 2 ++ .../andromeda-crowdfund/src/testing/tests.rs | 30 +++++++++++++------ .../src/crowdfund.rs | 10 ++++++- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs index bad349c2f..b2053584e 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs @@ -161,6 +161,7 @@ fn execute_add_tier(ctx: ExecuteContext, tier: Tier) -> Result Result Vec { vec![ Tier { level: Uint64::zero(), + label: "Basic Tier".to_string(), limit: None, price: Uint128::new(10u128), meta_data: TierMetaData { @@ -48,6 +49,7 @@ pub fn mock_campaign_tiers() -> Vec { }, Tier { level: Uint64::new(1u64), + label: "Tier 1".to_string(), limit: Some(Uint128::new(1000u128)), price: Uint128::new(10u128), meta_data: TierMetaData { diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs index aeebd7668..2cf88c526 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs @@ -71,6 +71,7 @@ fn test_instantiate_invalid_tiers() { let mut tiers = mock_campaign_tiers(); tiers.push(Tier { level: Uint64::new(1u64), + label: "Tier 1".to_string(), limit: Some(Uint128::new(100)), price: Uint128::zero(), meta_data: TierMetaData { @@ -113,6 +114,7 @@ fn test_add_tier() { let tier_to_add = Tier { level: Uint64::new(2u64), + label: "Tier 2".to_string(), limit: Some(Uint128::new(100)), price: Uint128::new(100), meta_data: TierMetaData { @@ -136,9 +138,10 @@ fn test_add_tier() { res, Response::new() .add_attribute("action", "add_tier") - .add_attribute("level", "2") - .add_attribute("price", "100") - .add_attribute("limit", "100") + .add_attribute("level", tier_to_add.level.to_string()) + .add_attribute("label", tier_to_add.label.clone()) + .add_attribute("price", tier_to_add.price.to_string()) + .add_attribute("limit", tier_to_add.limit.unwrap().to_string()) // Economics message .add_submessage(SubMsg::reply_on_error( CosmosMsg::Wasm(WasmMsg::Execute { @@ -171,7 +174,8 @@ fn test_add_tier_unauthorized() { let msg = ExecuteMsg::AddTier { tier: Tier { - level: Uint64::new(1u64), + level: Uint64::new(2u64), + label: "Tier 2".to_string(), limit: Some(Uint128::new(100)), price: Uint128::new(100), meta_data: TierMetaData { @@ -196,7 +200,8 @@ fn test_add_tier_zero_price() { let msg = ExecuteMsg::AddTier { tier: Tier { - level: Uint64::new(1u64), + level: Uint64::new(2u64), + label: "Tier 2".to_string(), limit: Some(Uint128::new(100)), price: Uint128::zero(), meta_data: TierMetaData { @@ -228,6 +233,7 @@ fn test_add_tier_duplicated() { let msg = ExecuteMsg::AddTier { tier: Tier { level: Uint64::zero(), + label: "Duplicated Tier".to_string(), limit: Some(Uint128::new(100u128)), price: Uint128::new(100u128), meta_data: TierMetaData { @@ -258,6 +264,7 @@ fn test_update_tier() { let updated_tier = Tier { level: Uint64::zero(), + label: "Tier 0".to_string(), limit: Some(Uint128::new(100)), price: Uint128::new(100), meta_data: TierMetaData { @@ -282,13 +289,15 @@ fn test_update_tier() { let info = mock_info("owner", &[]); let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!( res, Response::new() .add_attribute("action", "update_tier") - .add_attribute("level", "0") - .add_attribute("price", "100") - .add_attribute("limit", "100") + .add_attribute("level", updated_tier.level.to_string()) + .add_attribute("label", updated_tier.label.clone()) + .add_attribute("price", updated_tier.price.to_string()) + .add_attribute("limit", updated_tier.limit.unwrap().to_string()) // Economics message .add_submessage(SubMsg::reply_on_error( CosmosMsg::Wasm(WasmMsg::Execute { @@ -322,6 +331,7 @@ fn test_update_tier_unauthorized() { let msg = ExecuteMsg::UpdateTier { tier: Tier { level: Uint64::zero(), + label: "Tier 0".to_string(), limit: Some(Uint128::new(100)), price: Uint128::new(100), meta_data: TierMetaData { @@ -347,6 +357,7 @@ fn test_update_tier_zero_price() { let msg = ExecuteMsg::UpdateTier { tier: Tier { level: Uint64::zero(), + label: "Tier 0".to_string(), limit: Some(Uint128::new(100)), price: Uint128::zero(), meta_data: TierMetaData { @@ -378,6 +389,7 @@ fn test_update_tier_non_exist() { let msg = ExecuteMsg::UpdateTier { tier: Tier { level: Uint64::new(2u64), + label: "Tier 2".to_string(), limit: Some(Uint128::new(100u128)), price: Uint128::new(100u128), meta_data: TierMetaData { @@ -421,7 +433,7 @@ fn test_remove_tier() { res, Response::new() .add_attribute("action", "remove_tier") - .add_attribute("level", "0") + .add_attribute("level", level_to_remove.to_string()) // Economics message .add_submessage(SubMsg::reply_on_error( CosmosMsg::Wasm(WasmMsg::Execute { diff --git a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs index 5415f5d2d..34ece1e95 100644 --- a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs +++ b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs @@ -120,8 +120,8 @@ impl ToString for CampaignStage { #[cw_serde] pub struct Tier { - // TODO change to use string pub level: Uint64, + pub label: String, pub price: Uint128, pub limit: Option, // None for no limit pub meta_data: TierMetaData, @@ -143,6 +143,14 @@ impl Tier { msg: "Price can not be zero".to_string() } ); + ensure!( + !self.label.is_empty() && self.label.len() <= 64, + ContractError::InvalidTier { + operation: "all".to_string(), + msg: "Label should be no-empty and its length can be 64 at maximum".to_string() + } + ); + Ok(()) } } From 96549a67ae82bb8c42efd7fe425bcb5e0c347931 Mon Sep 17 00:00:00 2001 From: cowboy0015 Date: Tue, 7 May 2024 17:16:58 -0400 Subject: [PATCH 08/11] fix: refactored unit tests --- .../src/testing/mock_querier.rs | 19 +- .../andromeda-crowdfund/src/testing/tests.rs | 920 ++++++++---------- 2 files changed, 424 insertions(+), 515 deletions(-) diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs index d16897697..4d5846b25 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/mock_querier.rs @@ -16,7 +16,7 @@ use cosmwasm_std::{ pub const MOCK_TIER_CONTRACT: &str = "tier_contract"; pub const MOCK_WITHDRAWAL_ADDRESS: &str = "withdrawal_address"; - +pub const MOCK_DEFAULT_LIMIT: u128 = 100000; pub fn mock_campaign_config() -> CampaignConfig { CampaignConfig { title: "First Crowdfund".to_string(), @@ -50,7 +50,7 @@ pub fn mock_campaign_tiers() -> Vec { Tier { level: Uint64::new(1u64), label: "Tier 1".to_string(), - limit: Some(Uint128::new(1000u128)), + limit: Some(Uint128::new(MOCK_DEFAULT_LIMIT)), price: Uint128::new(10u128), meta_data: TierMetaData { extension: TokenExtension { @@ -62,6 +62,21 @@ pub fn mock_campaign_tiers() -> Vec { ] } +pub fn mock_zero_price_tier(level: Uint64) -> Tier { + Tier { + level, + label: "Invalid Tier".to_string(), + limit: None, + price: Uint128::zero(), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + token_uri: None, + }, + } +} + /// Alternative to `cosmwasm_std::testing::mock_dependencies` that allows us to respond to custom queries. /// /// Automatically assigns a kernel address as MOCK_KERNEL_CONTRACT. diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs index 2cf88c526..b1b3fecf7 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs @@ -1,9 +1,8 @@ use andromeda_non_fungible_tokens::{ - crowdfund::{ExecuteMsg, InstantiateMsg, Tier, TierMetaData, TierOrder}, + crowdfund::{CampaignConfig, ExecuteMsg, InstantiateMsg, Tier, TierMetaData, TierOrder}, cw721::TokenExtension, }; use andromeda_std::{ - ado_base::Module, common::{reply::ReplyId, MillisecondsExpiration}, error::ContractError, os::economics::ExecuteMsg as EconomicsExecuteMsg, @@ -11,25 +10,24 @@ use andromeda_std::{ }; use cosmwasm_std::{ testing::{mock_env, mock_info}, - to_json_binary, Addr, CosmosMsg, DepsMut, Order, Response, SubMsg, Uint128, Uint64, WasmMsg, + to_json_binary, Addr, CosmosMsg, DepsMut, Order, Response, Storage, SubMsg, Uint128, Uint64, + WasmMsg, }; use crate::{ contract::{execute, instantiate}, state::{CAMPAIGN_CONFIG, TIERS, TIER_ORDERS}, - testing::mock_querier::mock_dependencies_custom, + testing::mock_querier::{mock_dependencies_custom, mock_zero_price_tier, MOCK_DEFAULT_LIMIT}, }; use super::mock_querier::{mock_campaign_config, mock_campaign_tiers}; -fn init(deps: DepsMut, modules: Option>) -> Response { - let config = mock_campaign_config(); - let tiers = mock_campaign_tiers(); +fn init(deps: DepsMut, config: CampaignConfig, tiers: Vec) -> Response { let msg = InstantiateMsg { campaign_config: config, tiers, owner: None, - modules, + modules: None, kernel_address: MOCK_KERNEL_CONTRACT.to_string(), }; @@ -37,143 +35,107 @@ fn init(deps: DepsMut, modules: Option>) -> Response { instantiate(deps, mock_env(), info, msg).unwrap() } -#[test] -fn test_instantiate() { - let mut deps = mock_dependencies_custom(&[]); +fn get_tiers(storage: &dyn Storage) -> Vec { + TIERS + .range_raw(storage, None, None, Order::Ascending) + .map(|res| res.unwrap().1) + .collect() +} + +#[cfg(test)] +mod test { - let res = init(deps.as_mut(), None); + use super::*; - assert_eq!( + fn instantiate_response(owner: &str) -> Response { Response::new() .add_attribute("method", "instantiate") .add_attribute("type", "crowdfund") .add_attribute("kernel_address", MOCK_KERNEL_CONTRACT) - .add_attribute("owner", "owner"), - res - ); - - assert_eq!( - mock_campaign_config(), - CAMPAIGN_CONFIG.load(deps.as_mut().storage).unwrap() - ); - - let tiers: Vec = TIERS - .range_raw(deps.as_ref().storage, None, None, Order::Ascending) - .map(|res| res.unwrap().1) - .collect(); - assert_eq!(mock_campaign_tiers(), tiers); -} - -#[test] -fn test_instantiate_invalid_tiers() { - let mut deps = mock_dependencies_custom(&[]); - let config = mock_campaign_config(); - let mut tiers = mock_campaign_tiers(); - tiers.push(Tier { - level: Uint64::new(1u64), - label: "Tier 1".to_string(), - limit: Some(Uint128::new(100)), - price: Uint128::zero(), - meta_data: TierMetaData { - extension: TokenExtension { - publisher: MOCK_ADO_PUBLISHER.to_string(), + .add_attribute("owner", owner) + } + + struct InstantiateTestCase { + config: CampaignConfig, + tiers: Vec, + expected_res: Result, + expected_config: Option, + expected_tiers: Vec, + } + #[test] + fn test_instantiate() { + let test_cases: Vec = vec![ + InstantiateTestCase { + config: mock_campaign_config(), + tiers: mock_campaign_tiers(), + expected_res: Ok(instantiate_response("owner")), + expected_config: Some(mock_campaign_config()), + expected_tiers: mock_campaign_tiers(), }, - token_uri: None, - }, - }); - let msg = InstantiateMsg { - campaign_config: config, - tiers, - owner: None, - kernel_address: MOCK_KERNEL_CONTRACT.to_string(), - modules: None, - }; - - let info = mock_info("owner", &[]); - let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - - assert_eq!( - res, - ContractError::InvalidTier { - operation: "all".to_string(), - msg: "Price can not be zero".to_string() - } - ); - - assert_eq!( - mock_campaign_config(), - CAMPAIGN_CONFIG.load(deps.as_mut().storage).unwrap() - ); -} - -#[test] -fn test_add_tier() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); - - let tier_to_add = Tier { - level: Uint64::new(2u64), - label: "Tier 2".to_string(), - limit: Some(Uint128::new(100)), - price: Uint128::new(100), - meta_data: TierMetaData { - extension: TokenExtension { - publisher: MOCK_ADO_PUBLISHER.to_string(), + InstantiateTestCase { + config: mock_campaign_config(), + tiers: vec![mock_zero_price_tier(Uint64::zero())], + expected_res: Err(ContractError::InvalidTier { + operation: "all".to_string(), + msg: "Price can not be zero".to_string(), + }), + expected_config: Some(mock_campaign_config()), + expected_tiers: vec![], }, - token_uri: None, - }, - }; - - // Tier with the same level does not exist before adding. - assert!(!TIERS.has(deps.as_ref().storage, tier_to_add.level.into())); - - let msg = ExecuteMsg::AddTier { - tier: tier_to_add.clone(), - }; + ]; + + for test in test_cases { + let mut deps = mock_dependencies_custom(&[]); + let info = mock_info("owner", &[]); + let msg = InstantiateMsg { + campaign_config: test.config, + tiers: test.tiers, + owner: None, + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + modules: None, + }; + let res = instantiate(deps.as_mut(), mock_env(), info, msg); + + assert_eq!(res, test.expected_res); + assert_eq!( + CAMPAIGN_CONFIG.load(deps.as_mut().storage).unwrap(), + test.expected_config.unwrap() + ); + assert_eq!(get_tiers(deps.as_ref().storage), test.expected_tiers); + } + } - let info = mock_info("owner", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - assert_eq!( - res, + fn add_tier_response(tier: &Tier, payee: &str) -> Response { Response::new() .add_attribute("action", "add_tier") - .add_attribute("level", tier_to_add.level.to_string()) - .add_attribute("label", tier_to_add.label.clone()) - .add_attribute("price", tier_to_add.price.to_string()) - .add_attribute("limit", tier_to_add.limit.unwrap().to_string()) + .add_attribute("level", tier.level.to_string()) + .add_attribute("label", tier.label.clone()) + .add_attribute("price", tier.price.to_string()) + .add_attribute("limit", tier.limit.unwrap().to_string()) // Economics message .add_submessage(SubMsg::reply_on_error( CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "economics_contract".to_string(), msg: to_json_binary(&EconomicsExecuteMsg::PayFee { - payee: Addr::unchecked("owner"), - action: "AddTier".to_string() + payee: Addr::unchecked(payee), + action: "AddTier".to_string(), }) .unwrap(), funds: vec![], }), ReplyId::PayFee.repr(), - )), - ); - - // Tier is saved in the state. - assert_eq!( - tier_to_add, - TIERS - .load(deps.as_ref().storage, tier_to_add.level.into()) - .unwrap() - ); -} - -#[test] -fn test_add_tier_unauthorized() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); - - let msg = ExecuteMsg::AddTier { - tier: Tier { + )) + } + + struct TierTestCase { + tier: Tier, + expected_res: Result, + payee: String, + } + + #[test] + fn test_add_tier() { + let valid_tier = Tier { level: Uint64::new(2u64), label: "Tier 2".to_string(), limit: Some(Uint128::new(100)), @@ -184,152 +146,96 @@ fn test_add_tier_unauthorized() { }, token_uri: None, }, - }, - }; - - let info = mock_info("owner1", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(err, ContractError::Unauthorized {}); -} - -#[test] -fn test_add_tier_zero_price() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); - - let msg = ExecuteMsg::AddTier { - tier: Tier { - level: Uint64::new(2u64), + }; + let duplicated_tier = Tier { + level: Uint64::new(0u64), label: "Tier 2".to_string(), limit: Some(Uint128::new(100)), - price: Uint128::zero(), + price: Uint128::new(100), meta_data: TierMetaData { extension: TokenExtension { publisher: MOCK_ADO_PUBLISHER.to_string(), }, token_uri: None, }, - }, - }; - - let info = mock_info("owner", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!( - err, - ContractError::InvalidTier { - operation: "all".to_string(), - msg: "Price can not be zero".to_string() - } - ); -} - -#[test] -fn test_add_tier_duplicated() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); + }; - let msg = ExecuteMsg::AddTier { - tier: Tier { - level: Uint64::zero(), - label: "Duplicated Tier".to_string(), - limit: Some(Uint128::new(100u128)), - price: Uint128::new(100u128), - meta_data: TierMetaData { - extension: TokenExtension { - publisher: MOCK_ADO_PUBLISHER.to_string(), - }, - token_uri: None, + let test_cases: Vec = vec![ + TierTestCase { + tier: valid_tier.clone(), + expected_res: Ok(add_tier_response(&valid_tier, "owner")), + payee: "owner".to_string(), }, - }, - }; - - let info = mock_info("owner", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!( - err, - ContractError::InvalidTier { - operation: "add".to_string(), - msg: "Tier with level 0 already exist".to_string() - } - ); -} - -#[test] -fn test_update_tier() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); - - let updated_tier = Tier { - level: Uint64::zero(), - label: "Tier 0".to_string(), - limit: Some(Uint128::new(100)), - price: Uint128::new(100), - meta_data: TierMetaData { - extension: TokenExtension { - publisher: MOCK_ADO_PUBLISHER.to_string(), + TierTestCase { + tier: valid_tier.clone(), + expected_res: Err(ContractError::Unauthorized {}), + payee: "owner1".to_string(), }, - token_uri: None, - }, - }; - - // Before updating, tier with same level exists but their data is different. - assert_ne!( - updated_tier, - TIERS - .load(deps.as_ref().storage, updated_tier.level.into()) - .unwrap() - ); - - let msg = ExecuteMsg::UpdateTier { - tier: updated_tier.clone(), - }; - - let info = mock_info("owner", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + TierTestCase { + tier: mock_zero_price_tier(Uint64::new(2)), + expected_res: Err(ContractError::InvalidTier { + operation: "all".to_string(), + msg: "Price can not be zero".to_string(), + }), + payee: "owner".to_string(), + }, + TierTestCase { + tier: duplicated_tier, + expected_res: Err(ContractError::InvalidTier { + operation: "add".to_string(), + msg: "Tier with level 0 already exist".to_string(), + }), + payee: "owner".to_string(), + }, + ]; + for test in test_cases { + let mut deps = mock_dependencies_custom(&[]); + let _ = init(deps.as_mut(), mock_campaign_config(), mock_campaign_tiers()); + + let info = mock_info(&test.payee, &[]); + + let msg = ExecuteMsg::AddTier { + tier: test.tier.clone(), + }; + + let res = execute(deps.as_mut(), mock_env(), info, msg); + assert_eq!(res, test.expected_res); + if res.is_ok() { + assert_eq!( + test.tier, + TIERS + .load(deps.as_ref().storage, test.tier.level.into()) + .unwrap() + ); + } + } + } - assert_eq!( - res, + fn update_tier_response(tier: &Tier, payee: &str) -> Response { Response::new() .add_attribute("action", "update_tier") - .add_attribute("level", updated_tier.level.to_string()) - .add_attribute("label", updated_tier.label.clone()) - .add_attribute("price", updated_tier.price.to_string()) - .add_attribute("limit", updated_tier.limit.unwrap().to_string()) + .add_attribute("level", tier.level.to_string()) + .add_attribute("label", tier.label.clone()) + .add_attribute("price", tier.price.to_string()) + .add_attribute("limit", tier.limit.unwrap().to_string()) // Economics message .add_submessage(SubMsg::reply_on_error( CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "economics_contract".to_string(), msg: to_json_binary(&EconomicsExecuteMsg::PayFee { - payee: Addr::unchecked("owner"), - action: "UpdateTier".to_string() + payee: Addr::unchecked(payee), + action: "UpdateTier".to_string(), }) .unwrap(), funds: vec![], }), ReplyId::PayFee.repr(), - )), - ); - - // Tier updated successfully. - assert_eq!( - updated_tier, - TIERS - .load(deps.as_ref().storage, updated_tier.level.into()) - .unwrap() - ); -} + )) + } -#[test] -fn test_update_tier_unauthorized() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); - - let msg = ExecuteMsg::UpdateTier { - tier: Tier { + #[test] + fn test_update_tier() { + let valid_tier = Tier { level: Uint64::zero(), label: "Tier 0".to_string(), limit: Some(Uint128::new(100)), @@ -340,315 +246,303 @@ fn test_update_tier_unauthorized() { }, token_uri: None, }, - }, - }; - - let info = mock_info("owner1", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(err, ContractError::Unauthorized {}); -} - -#[test] -fn test_update_tier_zero_price() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); - - let msg = ExecuteMsg::UpdateTier { - tier: Tier { - level: Uint64::zero(), - label: "Tier 0".to_string(), - limit: Some(Uint128::new(100)), - price: Uint128::zero(), - meta_data: TierMetaData { - extension: TokenExtension { - publisher: MOCK_ADO_PUBLISHER.to_string(), - }, - token_uri: None, - }, - }, - }; - - let info = mock_info("owner", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!( - err, - ContractError::InvalidTier { - operation: "all".to_string(), - msg: "Price can not be zero".to_string() - } - ); -} - -#[test] -fn test_update_tier_non_exist() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); - - let msg = ExecuteMsg::UpdateTier { - tier: Tier { + }; + let non_existing_tier = Tier { level: Uint64::new(2u64), label: "Tier 2".to_string(), - limit: Some(Uint128::new(100u128)), - price: Uint128::new(100u128), + limit: Some(Uint128::new(100)), + price: Uint128::new(100), meta_data: TierMetaData { extension: TokenExtension { publisher: MOCK_ADO_PUBLISHER.to_string(), }, token_uri: None, }, - }, - }; + }; - let info = mock_info("owner", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!( - err, - ContractError::InvalidTier { - operation: "update".to_string(), - msg: "Tier with level 2 does not exist".to_string() + let test_cases: Vec = vec![ + TierTestCase { + tier: valid_tier.clone(), + expected_res: Ok(update_tier_response(&valid_tier, "owner")), + payee: "owner".to_string(), + }, + TierTestCase { + tier: valid_tier.clone(), + expected_res: Err(ContractError::Unauthorized {}), + payee: "owner1".to_string(), + }, + TierTestCase { + tier: mock_zero_price_tier(Uint64::zero()), + expected_res: Err(ContractError::InvalidTier { + operation: "all".to_string(), + msg: "Price can not be zero".to_string(), + }), + payee: "owner".to_string(), + }, + TierTestCase { + tier: non_existing_tier, + expected_res: Err(ContractError::InvalidTier { + operation: "update".to_string(), + msg: "Tier with level 2 does not exist".to_string(), + }), + payee: "owner".to_string(), + }, + ]; + for test in test_cases { + let mut deps = mock_dependencies_custom(&[]); + let _ = init(deps.as_mut(), mock_campaign_config(), mock_campaign_tiers()); + + let info = mock_info(&test.payee, &[]); + + let msg = ExecuteMsg::UpdateTier { + tier: test.tier.clone(), + }; + + let res = execute(deps.as_mut(), mock_env(), info, msg); + assert_eq!(res, test.expected_res); + if res.is_ok() { + assert_eq!( + test.tier, + TIERS + .load(deps.as_ref().storage, test.tier.level.into()) + .unwrap() + ); + } } - ); -} - -#[test] -fn test_remove_tier() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); - - let level_to_remove = Uint64::zero(); + } - // The tier to be removed exists before removing - assert!(TIERS.has(deps.as_ref().storage, level_to_remove.into())); - - let msg = ExecuteMsg::RemoveTier { - level: level_to_remove, - }; - - let info = mock_info("owner", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - assert_eq!( - res, + fn remove_tier_response(level: Uint64, payee: &str) -> Response { Response::new() .add_attribute("action", "remove_tier") - .add_attribute("level", level_to_remove.to_string()) - // Economics message + .add_attribute("level", level.to_string()) .add_submessage(SubMsg::reply_on_error( CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "economics_contract".to_string(), msg: to_json_binary(&EconomicsExecuteMsg::PayFee { - payee: Addr::unchecked("owner"), - action: "RemoveTier".to_string() + payee: Addr::unchecked(payee), + action: "RemoveTier".to_string(), }) .unwrap(), funds: vec![], }), ReplyId::PayFee.repr(), - )), - ); - - // The tier removed successfully - assert!(!TIERS.has(deps.as_ref().storage, level_to_remove.into())); -} - -#[test] -fn test_remove_tier_unauthorized() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); - - let msg = ExecuteMsg::RemoveTier { - level: Uint64::zero(), - }; - - let info = mock_info("owner1", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!(err, ContractError::Unauthorized {}); -} - -#[test] -fn test_remove_tier_non_exist() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); - - let msg = ExecuteMsg::RemoveTier { - level: Uint64::new(2u64), - }; + )) + } + #[test] + fn test_remove_tier() { + let valid_tier = Tier { + level: Uint64::zero(), + label: "Tier 0".to_string(), + limit: Some(Uint128::new(100)), + price: Uint128::new(100), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + token_uri: None, + }, + }; + let non_existing_tier = Tier { + level: Uint64::new(2u64), + label: "Tier 2".to_string(), + limit: Some(Uint128::new(100)), + price: Uint128::new(100), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + token_uri: None, + }, + }; - let info = mock_info("owner", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - assert_eq!( - err, - ContractError::InvalidTier { - operation: "remove".to_string(), - msg: "Tier with level 2 does not exist".to_string() + let test_cases: Vec = vec![ + TierTestCase { + tier: valid_tier.clone(), + expected_res: Ok(remove_tier_response(valid_tier.level, "owner")), + payee: "owner".to_string(), + }, + TierTestCase { + tier: valid_tier.clone(), + expected_res: Err(ContractError::Unauthorized {}), + payee: "owner1".to_string(), + }, + TierTestCase { + tier: non_existing_tier, + expected_res: Err(ContractError::InvalidTier { + operation: "remove".to_string(), + msg: "Tier with level 2 does not exist".to_string(), + }), + payee: "owner".to_string(), + }, + ]; + for test in test_cases { + let mut deps = mock_dependencies_custom(&[]); + let _ = init(deps.as_mut(), mock_campaign_config(), mock_campaign_tiers()); + + let info = mock_info(&test.payee, &[]); + + let msg = ExecuteMsg::RemoveTier { + level: test.tier.level, + }; + + let res = execute(deps.as_mut(), mock_env(), info, msg); + assert_eq!(res, test.expected_res); + if res.is_ok() { + assert!(!TIERS.has(deps.as_ref().storage, test.tier.level.into())); + } } - ); -} - -#[test] -fn test_start_campaign() { - let mut deps = mock_dependencies_custom(&[]); + } - let _ = init(deps.as_mut(), None); - - let env = mock_env(); - let initial_limit = TIERS.load(&deps.storage, 1).unwrap().limit.unwrap(); - - let mock_orderer = Addr::unchecked("mock_orderer".to_string()); - let msg = ExecuteMsg::StartCampaign { - start_time: None, - end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 100), - presale: Some(vec![TierOrder { - amount: Uint128::new(100u128), - level: Uint64::new(1u64), - orderer: mock_orderer.clone(), - }]), - }; - - let info = mock_info("owner", &[]); - let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - let limit = TIERS.load(&deps.storage, 1).unwrap().limit.unwrap(); - - assert_eq!( - res, + fn start_campaign_response(payee: &str) -> Response { Response::new() .add_attribute("action", "start_campaign") .add_submessage(SubMsg::reply_on_error( CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "economics_contract".to_string(), msg: to_json_binary(&EconomicsExecuteMsg::PayFee { - payee: Addr::unchecked("owner"), - action: "StartCampaign".to_string() + payee: Addr::unchecked(payee), + action: "StartCampaign".to_string(), }) .unwrap(), funds: vec![], }), ReplyId::PayFee.repr(), - )), - ); - assert_eq!(initial_limit, limit + Uint128::new(100)); - - let order = TIER_ORDERS.load(&deps.storage, (mock_orderer, 1)).unwrap(); - assert_eq!(order, 100); -} - -#[test] -fn test_start_campaign_unauthorized() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); - - let env = mock_env(); - - let msg = ExecuteMsg::StartCampaign { - start_time: None, - end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 100), - presale: None, - }; - - let info = mock_info("owner1", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - - assert_eq!(err, ContractError::Unauthorized {}); -} - -#[test] -fn test_start_campaign_invalid_tiers() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); - - // Remove level 0 tier that has no limit - TIERS.remove(&mut deps.storage, 0); - let env = mock_env(); - - let msg = ExecuteMsg::StartCampaign { - start_time: None, - end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 100), - presale: None, - }; - - let info = mock_info("owner", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - - assert_eq!(err, ContractError::InvalidTiers {}); -} - -#[test] -fn test_start_campaign_invalid_presale() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); - - let env = mock_env(); + )) + } + + struct StartCampaignTestCase { + tiers: Vec, + presale: Option>, + start_time: Option, + end_time: MillisecondsExpiration, + expected_res: Result, + payee: String, + } + + #[test] + fn test_start_campaign() { + let mock_orderer = Addr::unchecked("mock_orderer".to_string()); + let valid_presale = vec![TierOrder { + amount: Uint128::new(100u128), + level: Uint64::new(1u64), + orderer: mock_orderer.clone(), + }]; - let msg = ExecuteMsg::StartCampaign { - start_time: None, - end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 100), - presale: Some(vec![TierOrder { + let invalid_presale = vec![TierOrder { amount: Uint128::new(100u128), level: Uint64::new(2u64), - orderer: Addr::unchecked("mock_orderer"), - }]), - }; - - let info = mock_info("owner", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + orderer: mock_orderer.clone(), + }]; - assert_eq!( - err, - ContractError::InvalidTier { - operation: "set_tier_orders".to_string(), - msg: "Tier with level 2 does not exist".to_string() + let invalid_tiers = vec![Tier { + level: Uint64::new(1u64), + label: "Tier 1".to_string(), + limit: Some(Uint128::new(1000u128)), + price: Uint128::new(10u128), + meta_data: TierMetaData { + extension: TokenExtension { + publisher: MOCK_ADO_PUBLISHER.to_string(), + }, + token_uri: None, + }, + }]; + + let env = mock_env(); + let test_cases: Vec = vec![ + StartCampaignTestCase { + tiers: mock_campaign_tiers(), + presale: Some(valid_presale.clone()), + start_time: None, + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 100), + payee: "owner".to_string(), + expected_res: Ok(start_campaign_response("owner")), + }, + StartCampaignTestCase { + tiers: mock_campaign_tiers(), + presale: Some(valid_presale.clone()), + start_time: None, + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 100), + payee: "owner1".to_string(), + expected_res: Err(ContractError::Unauthorized {}), + }, + StartCampaignTestCase { + tiers: invalid_tiers, + presale: Some(valid_presale.clone()), + start_time: None, + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 100), + payee: "owner".to_string(), + expected_res: Err(ContractError::InvalidTiers {}), + }, + StartCampaignTestCase { + tiers: mock_campaign_tiers(), + presale: Some(invalid_presale.clone()), + start_time: None, + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 100), + payee: "owner".to_string(), + expected_res: Err(ContractError::InvalidTier { + operation: "set_tier_orders".to_string(), + msg: "Tier with level 2 does not exist".to_string(), + }), + }, + StartCampaignTestCase { + tiers: mock_campaign_tiers(), + presale: Some(valid_presale.clone()), + start_time: None, + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() - 100), + payee: "owner".to_string(), + expected_res: Err(ContractError::StartTimeAfterEndTime {}), + }, + StartCampaignTestCase { + tiers: mock_campaign_tiers(), + presale: Some(valid_presale.clone()), + start_time: Some(MillisecondsExpiration::from_seconds( + env.block.time.seconds() + 1000, + )), + end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 500), + payee: "owner".to_string(), + expected_res: Err(ContractError::StartTimeAfterEndTime {}), + }, + ]; + for test in test_cases { + let mut deps = mock_dependencies_custom(&[]); + let _ = init(deps.as_mut(), mock_campaign_config(), test.tiers.clone()); + + let info = mock_info(&test.payee, &[]); + + let msg = ExecuteMsg::StartCampaign { + start_time: test.start_time, + end_time: test.end_time, + presale: test.presale.clone(), + }; + + let res = execute(deps.as_mut(), env.clone(), info, msg); + assert_eq!(res, test.expected_res); + + if res.is_ok() { + for order in &test.presale.unwrap() { + let order_amount: u128 = order.amount.into(); + assert_eq!( + TIER_ORDERS + .load(&deps.storage, (mock_orderer.clone(), order.level.into())) + .unwrap(), + order_amount + ); + let cur_limit = TIERS.load(&deps.storage, order.level.into()).unwrap().limit; + if cur_limit.is_some() { + assert_eq!( + TIERS + .load(&deps.storage, order.level.into()) + .unwrap() + .limit + .unwrap() + .u128(), + MOCK_DEFAULT_LIMIT - order_amount + ); + } + } + } } - ); -} - -#[test] -fn test_start_campaign_invalid_end_time() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); - - let env = mock_env(); - - let msg = ExecuteMsg::StartCampaign { - start_time: None, - end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() - 100), - presale: None, - }; - - let info = mock_info("owner", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - - assert_eq!(err, ContractError::StartTimeAfterEndTime {}); -} - -#[test] -fn test_start_campaign_invalid_start_time() { - let mut deps = mock_dependencies_custom(&[]); - - let _ = init(deps.as_mut(), None); - - let env = mock_env(); - - let msg = ExecuteMsg::StartCampaign { - start_time: Some(MillisecondsExpiration::from_seconds( - env.block.time.seconds() + 1000, - )), - end_time: MillisecondsExpiration::from_seconds(env.block.time.seconds() + 500), - presale: None, - }; - - let info = mock_info("owner", &[]); - let err = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); - - assert_eq!(err, ContractError::StartTimeAfterEndTime {}); + } } // #[test] From 3b51bc202d12322c94368da1054954694fb3bc19 Mon Sep 17 00:00:00 2001 From: cowboy0015 Date: Tue, 7 May 2024 17:38:46 -0400 Subject: [PATCH 09/11] fix: fixed start campaign test to check stage and campaign config --- .../andromeda-crowdfund/src/contract.rs | 2 +- .../andromeda-crowdfund/src/testing/tests.rs | 26 +++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs index b2053584e..358f4a197 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/contract.rs @@ -269,7 +269,7 @@ fn execute_start_campaign( } ); - // Update tier limit and update sender's order based on presale + // Update tier limit and update tier orders based on presale if let Some(presale) = presale { set_tier_orders(deps.storage, presale)?; } diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs index b1b3fecf7..f140c39f1 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs @@ -1,7 +1,10 @@ use andromeda_non_fungible_tokens::{ - crowdfund::{CampaignConfig, ExecuteMsg, InstantiateMsg, Tier, TierMetaData, TierOrder}, + crowdfund::{ + CampaignConfig, CampaignStage, ExecuteMsg, InstantiateMsg, Tier, TierMetaData, TierOrder, + }, cw721::TokenExtension, }; + use andromeda_std::{ common::{reply::ReplyId, MillisecondsExpiration}, error::ContractError, @@ -16,7 +19,7 @@ use cosmwasm_std::{ use crate::{ contract::{execute, instantiate}, - state::{CAMPAIGN_CONFIG, TIERS, TIER_ORDERS}, + state::{CAMPAIGN_CONFIG, CAMPAIGN_STAGE, TIERS, TIER_ORDERS}, testing::mock_querier::{mock_dependencies_custom, mock_zero_price_tier, MOCK_DEFAULT_LIMIT}, }; @@ -519,6 +522,18 @@ mod test { assert_eq!(res, test.expected_res); if res.is_ok() { + assert_eq!( + CAMPAIGN_CONFIG.load(&deps.storage).unwrap().start_time, + test.start_time + ); + assert_eq!( + CAMPAIGN_CONFIG.load(&deps.storage).unwrap().end_time, + test.end_time + ); + assert_eq!( + CAMPAIGN_STAGE.load(&deps.storage).unwrap(), + CampaignStage::ONGOING + ); for order in &test.presale.unwrap() { let order_amount: u128 = order.amount.into(); assert_eq!( @@ -540,6 +555,13 @@ mod test { ); } } + } else { + assert_eq!( + CAMPAIGN_STAGE + .load(&deps.storage) + .unwrap_or(CampaignStage::READY), + CampaignStage::READY + ); } } } From d72005036ef6a6c1e0a54c79730cba00a99ea1b5 Mon Sep 17 00:00:00 2001 From: cowboy0015 Date: Thu, 9 May 2024 10:52:57 -0400 Subject: [PATCH 10/11] fix: added name to each test cases to identify --- .../andromeda-crowdfund/src/testing/tests.rs | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs index f140c39f1..d2664ab7b 100644 --- a/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs +++ b/contracts/non-fungible-tokens/andromeda-crowdfund/src/testing/tests.rs @@ -59,6 +59,7 @@ mod test { } struct InstantiateTestCase { + name: String, config: CampaignConfig, tiers: Vec, expected_res: Result, @@ -69,6 +70,7 @@ mod test { fn test_instantiate() { let test_cases: Vec = vec![ InstantiateTestCase { + name: "standard instantiate".to_string(), config: mock_campaign_config(), tiers: mock_campaign_tiers(), expected_res: Ok(instantiate_response("owner")), @@ -76,6 +78,7 @@ mod test { expected_tiers: mock_campaign_tiers(), }, InstantiateTestCase { + name: "instantiate with invalid tiers including zero price tier".to_string(), config: mock_campaign_config(), tiers: vec![mock_zero_price_tier(Uint64::zero())], expected_res: Err(ContractError::InvalidTier { @@ -99,7 +102,7 @@ mod test { }; let res = instantiate(deps.as_mut(), mock_env(), info, msg); - assert_eq!(res, test.expected_res); + assert_eq!(res, test.expected_res, "Test case: {}", test.name); assert_eq!( CAMPAIGN_CONFIG.load(deps.as_mut().storage).unwrap(), test.expected_config.unwrap() @@ -131,6 +134,7 @@ mod test { } struct TierTestCase { + name: String, tier: Tier, expected_res: Result, payee: String, @@ -165,16 +169,19 @@ mod test { let test_cases: Vec = vec![ TierTestCase { + name: "standard add_tier".to_string(), tier: valid_tier.clone(), expected_res: Ok(add_tier_response(&valid_tier, "owner")), payee: "owner".to_string(), }, TierTestCase { + name: "add_tier with unauthorized sender".to_string(), tier: valid_tier.clone(), expected_res: Err(ContractError::Unauthorized {}), payee: "owner1".to_string(), }, TierTestCase { + name: "add_tier with zero price tier".to_string(), tier: mock_zero_price_tier(Uint64::new(2)), expected_res: Err(ContractError::InvalidTier { operation: "all".to_string(), @@ -183,6 +190,7 @@ mod test { payee: "owner".to_string(), }, TierTestCase { + name: "add_tier with duplicated tier".to_string(), tier: duplicated_tier, expected_res: Err(ContractError::InvalidTier { operation: "add".to_string(), @@ -202,7 +210,7 @@ mod test { }; let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(res, test.expected_res); + assert_eq!(res, test.expected_res, "Test case: {}", test.name); if res.is_ok() { assert_eq!( test.tier, @@ -265,16 +273,19 @@ mod test { let test_cases: Vec = vec![ TierTestCase { + name: "standard update_tier".to_string(), tier: valid_tier.clone(), expected_res: Ok(update_tier_response(&valid_tier, "owner")), payee: "owner".to_string(), }, TierTestCase { + name: "update_tier with unauthorized sender".to_string(), tier: valid_tier.clone(), expected_res: Err(ContractError::Unauthorized {}), payee: "owner1".to_string(), }, TierTestCase { + name: "update_tier with zero price tier".to_string(), tier: mock_zero_price_tier(Uint64::zero()), expected_res: Err(ContractError::InvalidTier { operation: "all".to_string(), @@ -283,6 +294,7 @@ mod test { payee: "owner".to_string(), }, TierTestCase { + name: "update_tier with non existing tier".to_string(), tier: non_existing_tier, expected_res: Err(ContractError::InvalidTier { operation: "update".to_string(), @@ -302,7 +314,7 @@ mod test { }; let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(res, test.expected_res); + assert_eq!(res, test.expected_res, "Test case: {}", test.name); if res.is_ok() { assert_eq!( test.tier, @@ -360,16 +372,19 @@ mod test { let test_cases: Vec = vec![ TierTestCase { + name: "standard remove_tier".to_string(), tier: valid_tier.clone(), expected_res: Ok(remove_tier_response(valid_tier.level, "owner")), payee: "owner".to_string(), }, TierTestCase { + name: "remove_tier with unauthorized sender".to_string(), tier: valid_tier.clone(), expected_res: Err(ContractError::Unauthorized {}), payee: "owner1".to_string(), }, TierTestCase { + name: "remove_tier with non existing tier level".to_string(), tier: non_existing_tier, expected_res: Err(ContractError::InvalidTier { operation: "remove".to_string(), @@ -389,7 +404,7 @@ mod test { }; let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(res, test.expected_res); + assert_eq!(res, test.expected_res, "Test case: {}", test.name); if res.is_ok() { assert!(!TIERS.has(deps.as_ref().storage, test.tier.level.into())); } @@ -414,6 +429,7 @@ mod test { } struct StartCampaignTestCase { + name: String, tiers: Vec, presale: Option>, start_time: Option, @@ -453,6 +469,7 @@ mod test { let env = mock_env(); let test_cases: Vec = vec![ StartCampaignTestCase { + name: "standard start_campaign".to_string(), tiers: mock_campaign_tiers(), presale: Some(valid_presale.clone()), start_time: None, @@ -461,6 +478,7 @@ mod test { expected_res: Ok(start_campaign_response("owner")), }, StartCampaignTestCase { + name: "start_campaign with unauthorized sender".to_string(), tiers: mock_campaign_tiers(), presale: Some(valid_presale.clone()), start_time: None, @@ -469,6 +487,7 @@ mod test { expected_res: Err(ContractError::Unauthorized {}), }, StartCampaignTestCase { + name: "start_campaign with no unlimited tier".to_string(), tiers: invalid_tiers, presale: Some(valid_presale.clone()), start_time: None, @@ -477,6 +496,7 @@ mod test { expected_res: Err(ContractError::InvalidTiers {}), }, StartCampaignTestCase { + name: "start_campaign with invalid presales".to_string(), tiers: mock_campaign_tiers(), presale: Some(invalid_presale.clone()), start_time: None, @@ -488,6 +508,7 @@ mod test { }), }, StartCampaignTestCase { + name: "start_campaign with invalid end_time".to_string(), tiers: mock_campaign_tiers(), presale: Some(valid_presale.clone()), start_time: None, @@ -496,6 +517,7 @@ mod test { expected_res: Err(ContractError::StartTimeAfterEndTime {}), }, StartCampaignTestCase { + name: "start_campaign with invalid start_time".to_string(), tiers: mock_campaign_tiers(), presale: Some(valid_presale.clone()), start_time: Some(MillisecondsExpiration::from_seconds( @@ -519,7 +541,7 @@ mod test { }; let res = execute(deps.as_mut(), env.clone(), info, msg); - assert_eq!(res, test.expected_res); + assert_eq!(res, test.expected_res, "Test case: {}", test.name); if res.is_ok() { assert_eq!( From 83b91a5c3b4bd2bcfbeea5ebc9b1969b85bb295f Mon Sep 17 00:00:00 2001 From: cowboy0015 Date: Thu, 9 May 2024 19:08:25 -0400 Subject: [PATCH 11/11] fix: fixed typo in crowdfund pacakge --- packages/andromeda-non-fungible-tokens/src/crowdfund.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs index 34ece1e95..879363d7e 100644 --- a/packages/andromeda-non-fungible-tokens/src/crowdfund.rs +++ b/packages/andromeda-non-fungible-tokens/src/crowdfund.rs @@ -157,7 +157,7 @@ impl Tier { #[cw_serde] pub struct TierMetaData { /// Universal resource identifier for the tier - /// Should point to a JSON file that conforms to the ERC721 + /// Should point to a JSON file that conforms to the CW721 /// Metadata JSON Schema pub token_uri: Option, /// Any custom extension used by this contract