From 779a908cb1857b547df98aefe4aa5adc45956506 Mon Sep 17 00:00:00 2001 From: Ninjatosba Date: Thu, 18 Jan 2024 19:36:56 +0300 Subject: [PATCH 01/11] add open edition minter execute funtions --- Cargo.lock | 34 ++ Cargo.toml | 3 +- .../factories/minter-factory/src/contract.rs | 6 +- contracts/minters/minter/src/contract.rs | 22 +- contracts/minters/minter/src/utils.rs | 1 - .../minters/open-edition-minter/Cargo.toml | 58 ++ .../open-edition-minter/src/bin/schema.rs | 13 + .../open-edition-minter/src/contract.rs | 557 ++++++++++++++++++ .../minters/open-edition-minter/src/error.rs | 89 +++ .../minters/open-edition-minter/src/lib.rs | 4 + .../minters/open-edition-minter/src/msg.rs | 14 + .../minters/open-edition-minter/src/state.rs | 17 + integration-tests/src/test_minter_creation.rs | 2 +- integration-tests/src/utils.rs | 3 +- packages/minter-types/src/lib.rs | 2 +- packages/open-edition-minter-types/Cargo.toml | 34 ++ packages/open-edition-minter-types/src/lib.rs | 81 +++ 17 files changed, 919 insertions(+), 21 deletions(-) create mode 100644 contracts/minters/open-edition-minter/Cargo.toml create mode 100644 contracts/minters/open-edition-minter/src/bin/schema.rs create mode 100644 contracts/minters/open-edition-minter/src/contract.rs create mode 100644 contracts/minters/open-edition-minter/src/error.rs create mode 100644 contracts/minters/open-edition-minter/src/lib.rs create mode 100644 contracts/minters/open-edition-minter/src/msg.rs create mode 100644 contracts/minters/open-edition-minter/src/state.rs create mode 100644 packages/open-edition-minter-types/Cargo.toml create mode 100644 packages/open-edition-minter-types/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d3a1c23..7ff42f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -762,6 +762,28 @@ dependencies = [ "thiserror", ] +[[package]] +name = "omniflix-open-edition-minter" +version = "0.0.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.1", + "minter-types", + "omniflix-round-whitelist", + "omniflix-std", + "open-edition-minter-types", + "schemars", + "serde", + "sha2 0.10.8", + "thiserror", + "whitelist-types", +] + [[package]] name = "omniflix-round-whitelist" version = "0.0.1" @@ -864,6 +886,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "open-edition-minter-types" +version = "0.0.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.3", + "cw721-base 0.18.0", + "serde", + "thiserror", +] + [[package]] name = "pkcs8" version = "0.10.2" diff --git a/Cargo.toml b/Cargo.toml index 09430ed..30f5ccb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,4 +34,5 @@ cw-ownable = "0.5.1" cosmwasm-storage = "1.5.0" serde = { version = "1.0.145", default-features = false, features = ["derive"] } whitelist-types = { version = "0.0.1", path = "packages/whitelist-types" } -minter-types = { version = "0.0.1", path = "packages/minter-types" } \ No newline at end of file +minter-types = { version = "0.0.1", path = "packages/minter-types" } +open-edition-minter-types = { version = "0.0.1", path = "packages/open-edition-minter-types" } \ No newline at end of file diff --git a/contracts/factories/minter-factory/src/contract.rs b/contracts/factories/minter-factory/src/contract.rs index 0ca487b..0dc4d34 100644 --- a/contracts/factories/minter-factory/src/contract.rs +++ b/contracts/factories/minter-factory/src/contract.rs @@ -286,7 +286,6 @@ mod tests { id: "collection_id".to_string(), extensible: true, nsfw: false, - num_tokens: 10, base_uri: "https://example.com/base".to_string(), uri: "https://example.com/collection".to_string(), uri_hash: "hash123".to_string(), @@ -305,6 +304,7 @@ mod tests { per_address_limit: 3, collection_details: collection_details.clone(), end_time: None, + num_tokens: 100, }, }; @@ -336,6 +336,7 @@ mod tests { per_address_limit: 3, collection_details: collection_details.clone(), end_time: None, + num_tokens: 100, }, }; @@ -400,6 +401,7 @@ mod tests { per_address_limit: 3, collection_details: collection_details.clone(), end_time: None, + num_tokens: 100, }, }; @@ -444,6 +446,7 @@ mod tests { per_address_limit: 3, collection_details: collection_details.clone(), end_time: None, + num_tokens: 100, }, }; @@ -479,6 +482,7 @@ mod tests { per_address_limit: 3, collection_details: collection_details.clone(), end_time: None, + num_tokens: 100, }) .unwrap(), funds: vec![Coin { diff --git a/contracts/minters/minter/src/contract.rs b/contracts/minters/minter/src/contract.rs index 2e9a0c7..ebe8c3e 100644 --- a/contracts/minters/minter/src/contract.rs +++ b/contracts/minters/minter/src/contract.rs @@ -86,7 +86,7 @@ pub fn instantiate( } // Check num_tokens - if msg.collection_details.num_tokens == 0 { + if msg.num_tokens == 0 { return Err(ContractError::InvalidNumTokens {}); } @@ -125,7 +125,7 @@ pub fn instantiate( let payment_collector = maybe_addr(deps.api, msg.payment_collector.clone())?.unwrap_or(info.sender.clone()); - let num_tokens = msg.collection_details.num_tokens; + let num_tokens = msg.num_tokens; let config = Config { per_address_limit: msg.per_address_limit, @@ -149,7 +149,6 @@ pub fn instantiate( schema: msg.collection_details.schema, symbol: msg.collection_details.symbol, id: msg.collection_details.id, - num_tokens: msg.collection_details.num_tokens, extensible: msg.collection_details.extensible, nsfw: msg.collection_details.nsfw, base_uri: msg.collection_details.base_uri, @@ -218,7 +217,7 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::Mint {} => execute_mint(deps, env, info, msg), + ExecuteMsg::Mint {} => execute_mint(deps, env, info), ExecuteMsg::MintAdmin { recipient, denom_id, @@ -237,12 +236,7 @@ pub fn execute( } } -pub fn execute_mint( - deps: DepsMut, - env: Env, - info: MessageInfo, - _msg: ExecuteMsg, -) -> Result { +pub fn execute_mint(deps: DepsMut, env: Env, info: MessageInfo) -> Result { let config = CONFIG.load(deps.storage)?; // Check if any tokens are left let total_tokens_remaining = TOTAL_TOKENS_REMAINING.load(deps.storage)?; @@ -359,7 +353,7 @@ pub fn execute_mint( // Create the mint message let mint_msg: CosmosMsg = MsgMintOnft { - data: "".to_string(), + data: collection.data, id: token_id.clone(), metadata: Some(metadata.clone()), denom_id: collection.id.clone(), @@ -465,7 +459,7 @@ pub fn execute_mint_admin( // Create the mint message let mint_msg: CosmosMsg = MsgMintOnft { - data: "".to_string(), + data: collection.data, id: denom_id.clone(), metadata: Some(metadata), denom_id: collection.id.clone(), @@ -583,8 +577,8 @@ pub fn execute_randomize_list( // Add the (key, value) tuple to the vector mintable_tokens.push((key, value)); } - - let randomized_list = randomize_token_list(mintable_tokens, collection.num_tokens, env)?; + let tokens_remaining = TOTAL_TOKENS_REMAINING.load(deps.storage)?; + let randomized_list = randomize_token_list(mintable_tokens, tokens_remaining, env)?; for token in randomized_list { MINTABLE_TOKENS.save(deps.storage, token.0, &token.1)?; diff --git a/contracts/minters/minter/src/utils.rs b/contracts/minters/minter/src/utils.rs index ce7a101..febabc2 100644 --- a/contracts/minters/minter/src/utils.rs +++ b/contracts/minters/minter/src/utils.rs @@ -32,7 +32,6 @@ pub fn randomize_token_list( .map_err(StdError::generic_err)?; // Iterate over tokens let mut randomized_tokens: Vec<(u32, Token)> = Vec::new(); - // TODO: is it ok to reset all keys for every randomization? let mut key: u32 = 1; for token in raw_tokens { randomized_tokens.push((key, token)); diff --git a/contracts/minters/open-edition-minter/Cargo.toml b/contracts/minters/open-edition-minter/Cargo.toml new file mode 100644 index 0000000..3defcd4 --- /dev/null +++ b/contracts/minters/open-edition-minter/Cargo.toml @@ -0,0 +1,58 @@ +[package] +authors = ["Adnan Deniz Corlu "] +name = "omniflix-open-edition-minter" +version = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[profile.release] +codegen-units = 1 +debug = false +debug-assertions = false +incremental = false +lto = true +opt-level = 3 +overflow-checks = true +panic = 'abort' +rpath = false + +[features] +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[[bin]] +name = "schema" +path = "src/bin/schema.rs" +doc = false + +[dependencies] +cosmwasm-storage = { workspace = true } +omniflix-std = { workspace=true } +thiserror = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-controllers = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true } +minter-types={ workspace = true } +sha2 = { version = "0.10.2", default-features = false } +whitelist-types ={ workspace = true } +open-edition-minter-types = {workspace=true} +omniflix-round-whitelist = {path="../../whitelists/round-whitelist"} diff --git a/contracts/minters/open-edition-minter/src/bin/schema.rs b/contracts/minters/open-edition-minter/src/bin/schema.rs new file mode 100644 index 0000000..b7fc3a9 --- /dev/null +++ b/contracts/minters/open-edition-minter/src/bin/schema.rs @@ -0,0 +1,13 @@ +use cosmwasm_schema::write_api; + +use open_edition_minter_types::{InstantiateMsg, QueryMsg}; + +use omniflix_open_edition_minter::msg::ExecuteMsg; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/minters/open-edition-minter/src/contract.rs b/contracts/minters/open-edition-minter/src/contract.rs new file mode 100644 index 0000000..b209a65 --- /dev/null +++ b/contracts/minters/open-edition-minter/src/contract.rs @@ -0,0 +1,557 @@ +use std::str::FromStr; + +//use crate::msg::ExecuteMsg; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Order, Response, + StdResult, Uint128, WasmMsg, +}; +use cw_utils::{maybe_addr, must_pay, nonpayable}; +use open_edition_minter_types::{CollectionDetails, Config, InstantiateMsg, Token, UserDetails}; + +use crate::error::ContractError; +use crate::msg::ExecuteMsg; +use crate::state::{last_token_id, COLLECTION, CONFIG, MINTED_COUNT, MINTED_TOKENS}; +use cw2::set_contract_version; +use omniflix_round_whitelist::msg::ExecuteMsg as RoundWhitelistExecuteMsg; +use omniflix_std::types::omniflix::onft::v1beta1::{ + Metadata, MsgCreateDenom, MsgMintOnft, OnftQuerier, +}; +use whitelist_types::{ + IsActiveResponse, IsMemberResponse, MintPriceResponse, RoundWhitelistQueryMsgs, +}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:omniflix-minter-open-edition-minter"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg(not(test))] +#[allow(dead_code)] +const CREATION_FEE: Uint128 = Uint128::new(0); +#[allow(dead_code)] +#[cfg(not(test))] +const CREATION_FEE_DENOM: &str = ""; + +#[cfg(test)] +const CREATION_FEE: Uint128 = Uint128::new(100_000_000); +#[cfg(test)] +const CREATION_FEE_DENOM: &str = "uflix"; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + // Query denom creation fee + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + // Query factory params of instantiator + // If the instantiator is not a our factory then we wont be able to parse the response + // let _factory_params: ParamsResponse = deps + // .querier + // .query_wasm_smart(info.sender.clone().into_string(), &QueryFactoryParams {})?; + + // This field is implemented only for testing purposes + let creation_fee_amount = if CREATION_FEE == Uint128::new(0) { + let onft_querier = OnftQuerier::new(&deps.querier); + let params = onft_querier.params()?; + Uint128::from_str(¶ms.params.unwrap().denom_creation_fee.unwrap().amount)? + } else { + CREATION_FEE + }; + let creation_fee_denom = if CREATION_FEE_DENOM.is_empty() { + let onft_querier = OnftQuerier::new(&deps.querier); + let params = onft_querier.params()?; + params.params.unwrap().denom_creation_fee.unwrap().denom + } else { + CREATION_FEE_DENOM.to_string() + }; + + let amount = must_pay(&info, &creation_fee_denom)?; + // Exact amount must be paid + if amount != creation_fee_amount { + return Err(ContractError::InvalidCreationFee { + expected: amount, + sent: amount, + }); + } + // Check if per address limit is 0 + if msg.per_address_limit == 0 { + return Err(ContractError::PerAddressLimitZero {}); + } + // Check if token limit is 0 + if let Some(token_limit) = msg.token_limit { + if token_limit == 0 { + return Err(ContractError::InvalidNumTokens {}); + } + } + + // Check start time + if msg.start_time < env.block.time { + return Err(ContractError::InvalidStartTime {}); + } + // Check end time + if let Some(end_time) = msg.end_time { + if end_time < msg.start_time { + return Err(ContractError::InvalidEndTime {}); + } + } + + // Check royalty ratio we expect decimal number + let royalty_ratio = Decimal::from_str(&msg.royalty_ratio)?; + if royalty_ratio < Decimal::zero() || royalty_ratio > Decimal::one() { + return Err(ContractError::InvalidRoyaltyRatio {}); + } + // Check if whitelist already active + if let Some(whitelist_address) = msg.whitelist_address.clone() { + let is_active: IsActiveResponse = deps.querier.query_wasm_smart( + whitelist_address.clone(), + &RoundWhitelistQueryMsgs::IsActive {}, + )?; + if is_active.is_active { + return Err(ContractError::WhitelistAlreadyActive {}); + } + } + let admin = maybe_addr(deps.api, msg.admin.clone())?.unwrap_or(info.sender.clone()); + + let payment_collector = + maybe_addr(deps.api, msg.payment_collector.clone())?.unwrap_or(info.sender.clone()); + + let config = Config { + per_address_limit: msg.per_address_limit, + payment_collector, + start_time: msg.start_time, + royalty_ratio, + admin, + mint_price: Coin { + denom: msg.mint_denom.clone(), + amount: msg.mint_price, + }, + whitelist_address: maybe_addr(deps.api, msg.whitelist_address.clone())?, + end_time: msg.end_time, + token_limit: msg.token_limit, + }; + CONFIG.save(deps.storage, &config)?; + + let collection = CollectionDetails { + name: msg.collection_details.name, + description: msg.collection_details.description, + preview_uri: msg.collection_details.preview_uri, + schema: msg.collection_details.schema, + symbol: msg.collection_details.symbol, + id: msg.collection_details.id, + extensible: msg.collection_details.extensible, + nsfw: msg.collection_details.nsfw, + base_uri: msg.collection_details.base_uri, + uri: msg.collection_details.uri, + uri_hash: msg.collection_details.uri_hash, + data: msg.collection_details.data, + }; + COLLECTION.save(deps.storage, &collection)?; + + let nft_creation_msg: CosmosMsg = MsgCreateDenom { + description: collection.description, + id: collection.id, + name: collection.name, + preview_uri: collection.preview_uri, + schema: collection.schema, + sender: env.contract.address.into_string(), + symbol: collection.symbol, + data: collection.data, + uri: collection.uri, + uri_hash: collection.uri_hash, + creation_fee: Some( + Coin { + denom: creation_fee_denom, + amount: creation_fee_amount, + } + .into(), + ), + } + .into(); + + let res = Response::new() + .add_message(nft_creation_msg) + .add_attribute("action", "instantiate"); + + Ok(res) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Mint {} => execute_mint(deps, env, info), + ExecuteMsg::MintAdmin { recipient } => execute_mint_admin(deps, env, info, recipient), + ExecuteMsg::UpdateRoyaltyRatio { ratio } => { + execute_update_royalty_ratio(deps, env, info, ratio) + } + ExecuteMsg::UpdateMintPrice { mint_price } => { + execute_update_mint_price(deps, env, info, mint_price) + } + ExecuteMsg::UpdateWhitelistAddress { address } => { + execute_update_whitelist_address(deps, env, info, address) + } + } +} + +pub fn execute_mint(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + let config = CONFIG.load(deps.storage)?; + // Check if any token limit set and if it is reached + if let Some(token_limit) = config.token_limit { + if MINTED_COUNT.load(deps.storage)? >= token_limit { + return Err(ContractError::TokenLimitReached {}); + } + } + // Check if end time is determined and if it is passed + if let Some(end_time) = config.end_time { + if env.block.time > end_time { + return Err(ContractError::PublicMintingEnded {}); + } + } + + let mut user_details = MINTED_TOKENS + .may_load(deps.storage, info.sender.clone())? + .unwrap_or(UserDetails::default()); + + // Increment total minted count + user_details.total_minted_count += 1; + // Check if address has reached the limit + if user_details.total_minted_count > config.per_address_limit { + return Err(ContractError::AddressReachedMintLimit {}); + } + let mut mint_price = config.mint_price; + // Check if minting is started + + let is_public = env.block.time > config.start_time; + + let mut messages: Vec = vec![]; + + if !is_public { + // Check if any whitelist is present + if let Some(whitelist_address) = config.whitelist_address { + let is_active: IsActiveResponse = deps.querier.query_wasm_smart( + whitelist_address.clone().into_string(), + &RoundWhitelistQueryMsgs::IsActive {}, + )?; + if !is_active.is_active { + return Err(ContractError::WhitelistNotActive {}); + } + // Check whitelist price + let whitelist_price_response: MintPriceResponse = deps.querier.query_wasm_smart( + whitelist_address.clone().into_string(), + &RoundWhitelistQueryMsgs::Price {}, + )?; + mint_price = whitelist_price_response.mint_price; + // Check if member is whitelisted + let is_member_response: IsMemberResponse = deps.querier.query_wasm_smart( + whitelist_address.clone().into_string(), + &RoundWhitelistQueryMsgs::IsMember { + address: info.sender.clone().into_string(), + }, + )?; + if !is_member_response.is_member { + return Err(ContractError::AddressNotWhitelisted {}); + } + messages.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: whitelist_address.into_string(), + msg: to_json_binary(&RoundWhitelistExecuteMsg::PrivateMint { + collector: info.sender.clone().into_string(), + })?, + funds: vec![], + })); + } else { + return Err(ContractError::MintingNotStarted { + start_time: config.start_time, + current_time: env.block.time, + }); + }; + } + + // Check the payment + let amount = must_pay(&info, &mint_price.denom)?; + // Exact amount must be paid + if amount != mint_price.amount { + return Err(ContractError::IncorrectPaymentAmount { + expected: mint_price.amount, + sent: amount, + }); + } + // Get the payment collector address + let payment_collector = config.payment_collector; + let collection = COLLECTION.load(deps.storage)?; + + MINTED_COUNT.update(deps.storage, |mut total_tokens| -> StdResult<_> { + total_tokens += 1; + Ok(total_tokens) + })?; + + // Save the user details + MINTED_TOKENS.save(deps.storage, info.sender.clone(), &user_details)?; + + let token_id = last_token_id(deps.storage) + 1; + // Generate the metadata + let metadata = Metadata { + name: collection.name, + description: collection.description, + media_uri: collection.base_uri, + preview_uri: collection.preview_uri, + uri_hash: collection.uri_hash, + }; + + // Create the mint message + let mint_msg: CosmosMsg = MsgMintOnft { + data: collection.data, + id: token_id.clone().to_string(), + metadata: Some(metadata.clone()), + denom_id: collection.id.clone(), + transferable: true, + sender: env.contract.address.clone().into_string(), + extensible: collection.extensible, + nsfw: collection.nsfw, + recipient: info.sender.clone().into_string(), + royalty_share: config.royalty_ratio.atomics().to_string(), + } + .into(); + + // Create the Bank send message + let bank_msg: CosmosMsg = CosmosMsg::Bank(cosmwasm_std::BankMsg::Send { + to_address: payment_collector.into_string(), + amount: vec![Coin { + denom: mint_price.denom, + amount: mint_price.amount, + }], + }); + + messages.push(mint_msg.clone()); + messages.push(bank_msg.clone()); + + let res = Response::new() + .add_messages(messages) + .add_attribute("action", "mint") + .add_attribute("token_id", token_id.to_string()) + .add_attribute("collection_id", collection.id); + + Ok(res) +} + +pub fn execute_mint_admin( + deps: DepsMut, + env: Env, + info: MessageInfo, + recipient: String, +) -> Result { + // Check if sender is admin + nonpayable(&info)?; + + let config = CONFIG.load(deps.storage)?; + + let collection = COLLECTION.load(deps.storage)?; + + if info.sender != config.admin { + return Err(ContractError::Unauthorized {}); + } + let recipient = deps.api.addr_validate(&recipient)?; + // We are not checking token limit nor end time here because this is admin minting + + let token_id = last_token_id(deps.storage) + 1; + // Generate the metadata + + let metadata = Metadata { + name: collection.name, + description: collection.description, + media_uri: collection.base_uri, + preview_uri: collection.preview_uri, + uri_hash: collection.uri_hash, + }; + + // Create the mint message + let mint_msg: CosmosMsg = MsgMintOnft { + data: collection.data, + id: collection.id.clone(), + metadata: Some(metadata), + denom_id: collection.id.clone(), + transferable: true, + sender: env.contract.address.into_string(), + extensible: collection.extensible, + nsfw: collection.nsfw, + recipient: recipient.into_string(), + royalty_share: config.royalty_ratio.atomics().to_string(), + } + .into(); + + let res = Response::new() + .add_message(mint_msg) + .add_attribute("action", "mint") + .add_attribute("token_id", token_id.to_string()) + .add_attribute("denom_id", collection.id); + Ok(res) +} +pub fn execute_burn_remaining_tokens( + deps: DepsMut, + _env: Env, + info: MessageInfo, +) -> Result { + // Check if sender is admin + let config = CONFIG.load(deps.storage)?; + if info.sender != config.admin { + return Err(ContractError::Unauthorized {}); + } + // We cannot burn open edition minter but we can set token limit to 0 + let mut config = CONFIG.load(deps.storage)?; + config.token_limit = Some(0); + CONFIG.save(deps.storage, &config)?; + + let res = Response::new().add_attribute("action", "burn_remaining_tokens"); + Ok(res) +} + +pub fn execute_update_royalty_ratio( + deps: DepsMut, + _env: Env, + info: MessageInfo, + ratio: String, +) -> Result { + // Check if sender is admin + let mut config = CONFIG.load(deps.storage)?; + if info.sender != config.admin { + return Err(ContractError::Unauthorized {}); + } + // Check if ratio is decimal number + let ratio = Decimal::from_str(&ratio)?; + + if ratio < Decimal::zero() || ratio > Decimal::one() { + return Err(ContractError::InvalidRoyaltyRatio {}); + } + config.royalty_ratio = ratio; + + CONFIG.save(deps.storage, &config)?; + + let res = Response::new() + .add_attribute("action", "update_royalty_ratio") + .add_attribute("ratio", ratio.to_string()); + Ok(res) +} + +pub fn execute_update_mint_price( + deps: DepsMut, + env: Env, + info: MessageInfo, + mint_price: Uint128, +) -> Result { + // Check if sender is admin + let mut config = CONFIG.load(deps.storage)?; + if info.sender != config.admin { + return Err(ContractError::Unauthorized {}); + } + // Check if trading has started + if env.block.time > config.start_time { + return Err(ContractError::MintingAlreadyStarted {}); + } + // Check if mint price is valid + if mint_price == Uint128::new(0) { + return Err(ContractError::InvalidMintPrice {}); + } + config.mint_price.amount = mint_price; + + CONFIG.save(deps.storage, &config)?; + + let res = Response::new() + .add_attribute("action", "update_mint_price") + .add_attribute("mint_price", mint_price.to_string()); + Ok(res) +} + +pub fn execute_update_whitelist_address( + deps: DepsMut, + _env: Env, + info: MessageInfo, + address: String, +) -> Result { + // Check if sender is admin + let mut config = CONFIG.load(deps.storage)?; + if info.sender != config.admin { + return Err(ContractError::Unauthorized {}); + } + let whitelist_address = config.whitelist_address.clone(); + // Check if whitelist already active + let is_active: bool = deps.querier.query_wasm_smart( + whitelist_address.clone().unwrap().into_string(), + &RoundWhitelistQueryMsgs::IsActive {}, + )?; + if is_active { + return Err(ContractError::WhitelistAlreadyActive {}); + } + let address = deps.api.addr_validate(&address)?; + let is_active: bool = deps.querier.query_wasm_smart( + address.clone().into_string(), + &RoundWhitelistQueryMsgs::IsActive {}, + )?; + if is_active { + return Err(ContractError::WhitelistAlreadyActive {}); + } + config.whitelist_address = Some(address.clone()); + + CONFIG.save(deps.storage, &config)?; + + let res = Response::new() + .add_attribute("action", "update_whitelist_address") + .add_attribute("address", address.to_string()); + Ok(res) +} + +// // Implement Queries +// #[cfg_attr(not(feature = "library"), entry_point)] +// pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { +// match msg { +// QueryMsg::Collection {} => to_json_binary(&query_collection(deps, env)?), +// QueryMsg::Config {} => to_json_binary(&query_config(deps, env)?), +// QueryMsg::MintableTokens {} => to_json_binary(&query_mintable_tokens(deps, env)?), +// QueryMsg::MintedTokens { address } => { +// to_json_binary(&query_minted_tokens(deps, env, address)?) +// } +// QueryMsg::TotalTokens {} => to_json_binary(&query_total_tokens(deps, env)?), +// } +// } + +// fn query_collection(deps: Deps, _env: Env) -> Result { +// let collection = COLLECTION.load(deps.storage)?; +// Ok(collection) +// } + +// fn query_config(deps: Deps, _env: Env) -> Result { +// let config = CONFIG.load(deps.storage)?; +// Ok(config) +// } + +// fn query_mintable_tokens(deps: Deps, _env: Env) -> Result, ContractError> { +// let mut mintable_tokens: Vec = Vec::new(); +// for item in MINTABLE_TOKENS.range(deps.storage, None, None, Order::Ascending) { +// let (_key, value) = item?; + +// // Add the (key, value) tuple to the vector +// mintable_tokens.push(value); +// } +// Ok(mintable_tokens) +// } + +// fn query_minted_tokens( +// deps: Deps, +// _env: Env, +// address: String, +// ) -> Result { +// let address = deps.api.addr_validate(&address)?; +// let minted_tokens = MINTED_TOKENS.load(deps.storage, address)?; +// Ok(minted_tokens) +// } + +// fn query_total_tokens(deps: Deps, _env: Env) -> Result { +// let total_tokens = TOTAL_TOKENS_REMAINING.load(deps.storage)?; +// Ok(total_tokens) +// } diff --git a/contracts/minters/open-edition-minter/src/error.rs b/contracts/minters/open-edition-minter/src/error.rs new file mode 100644 index 0000000..e8e4b4c --- /dev/null +++ b/contracts/minters/open-edition-minter/src/error.rs @@ -0,0 +1,89 @@ +use std::convert::Infallible; + +use cosmwasm_std::{CheckedFromRatioError, ConversionOverflowError, StdError, Timestamp, Uint128}; +use cw_utils::PaymentError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Payment error")] + PaymentError(#[from] PaymentError), + + #[error("Overflow error")] + OverflowError {}, + + #[error("Divide by zero")] + DivideByZero {}, + + #[error("Invalid creation fee")] + InvalidCreationFee { expected: Uint128, sent: Uint128 }, + + #[error("Minting has not started yet")] + MintingNotStarted { + start_time: Timestamp, + current_time: Timestamp, + }, + + #[error("Minting has already started")] + MintingAlreadyStarted {}, + + #[error("Incorrect payment amount")] + IncorrectPaymentAmount { expected: Uint128, sent: Uint128 }, + + #[error("No tokens left to mint")] + NoTokensLeftToMint {}, + + #[error("Address has reached the mint limit")] + AddressReachedMintLimit {}, + + #[error("Token id is not mintable")] + TokenIdNotMintable {}, + + #[error("Per address limit cannot be zero")] + PerAddressLimitZero {}, + + #[error("Invalid number of tokens")] + InvalidNumTokens {}, + + #[error("Invalid royalty ratio")] + InvalidRoyaltyRatio {}, + + #[error("Invalid mint price")] + InvalidMintPrice {}, + + #[error("Invalid start time")] + InvalidStartTime {}, + + #[error("Invalid end time")] + InvalidEndTime {}, + + #[error("Address is not whitelisted")] + AddressNotWhitelisted {}, + + #[error("Whitelist is not active")] + WhitelistNotActive {}, + + #[error("Whitelist is already active")] + WhitelistAlreadyActive {}, + + #[error("Round start time is invalid")] + RoundStartTimeInvalid {}, + + #[error("Collection not found")] + CollectionNotFound {}, + + #[error("Error saving tokens")] + ErrorSavingTokens {}, + + #[error("Public minting ended")] + PublicMintingEnded {}, + + #[error("Token limit reached")] + TokenLimitReached {}, +} diff --git a/contracts/minters/open-edition-minter/src/lib.rs b/contracts/minters/open-edition-minter/src/lib.rs new file mode 100644 index 0000000..a5abdbb --- /dev/null +++ b/contracts/minters/open-edition-minter/src/lib.rs @@ -0,0 +1,4 @@ +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; diff --git a/contracts/minters/open-edition-minter/src/msg.rs b/contracts/minters/open-edition-minter/src/msg.rs new file mode 100644 index 0000000..44688bb --- /dev/null +++ b/contracts/minters/open-edition-minter/src/msg.rs @@ -0,0 +1,14 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Uint128; + +#[cw_serde] +pub enum ExecuteMsg { + Mint {}, + MintAdmin { recipient: String }, + UpdateRoyaltyRatio { ratio: String }, + UpdateMintPrice { mint_price: Uint128 }, + UpdateWhitelistAddress { address: String }, +} + +#[cw_serde] +pub enum QueryMsg {} diff --git a/contracts/minters/open-edition-minter/src/state.rs b/contracts/minters/open-edition-minter/src/state.rs new file mode 100644 index 0000000..bcc5261 --- /dev/null +++ b/contracts/minters/open-edition-minter/src/state.rs @@ -0,0 +1,17 @@ +use std::u32; + +use cosmwasm_std::{Addr, Storage}; +use cw_storage_plus::{Item, Map}; + +use open_edition_minter_types::{CollectionDetails, Config, Token, UserDetails}; + +pub const CONFIG: Item = Item::new("config"); +pub const COLLECTION: Item = Item::new("collection"); +pub const MINTED_COUNT: Item = Item::new("minted_count"); +// Address and number of tokens minted +pub const MINTED_TOKENS: Map = Map::new("minted_tokens"); + +pub fn last_token_id(store: &mut dyn Storage) -> u32 { + let minted_count = MINTED_COUNT.load(store).unwrap_or_default(); + minted_count +} diff --git a/integration-tests/src/test_minter_creation.rs b/integration-tests/src/test_minter_creation.rs index 6dc70a2..c679a9d 100644 --- a/integration-tests/src/test_minter_creation.rs +++ b/integration-tests/src/test_minter_creation.rs @@ -121,7 +121,7 @@ mod test_minter_creation { // Send 0 num tokens let mut minter_inst_msg = return_minter_instantiate_msg(); - minter_inst_msg.collection_details.num_tokens = 0; + minter_inst_msg.num_tokens = 0; let create_minter_msg = FactoryExecuteMsg::CreateMinter { msg: minter_inst_msg, }; diff --git a/integration-tests/src/utils.rs b/integration-tests/src/utils.rs index 255517e..bd79d59 100644 --- a/integration-tests/src/utils.rs +++ b/integration-tests/src/utils.rs @@ -33,14 +33,12 @@ pub fn return_minter_instantiate_msg() -> MinterInstantiateMsg { id: "id".to_string(), extensible: true, nsfw: false, - num_tokens: 1000, base_uri: "base_uri".to_string(), uri: "uri".to_string(), uri_hash: "uri_hash".to_string(), data: "data".to_string(), }; - MinterInstantiateMsg { per_address_limit: 1, admin: Some("creator".to_string()), @@ -52,6 +50,7 @@ pub fn return_minter_instantiate_msg() -> MinterInstantiateMsg { payment_collector: Some("payment_collector".to_string()), whitelist_address: None, end_time: Some(Timestamp::from_nanos(1_000_000_000 + 1_000_000_000)), + num_tokens: 1000, } } diff --git a/packages/minter-types/src/lib.rs b/packages/minter-types/src/lib.rs index 8c89cc0..dcda7b1 100644 --- a/packages/minter-types/src/lib.rs +++ b/packages/minter-types/src/lib.rs @@ -18,6 +18,7 @@ pub struct InstantiateMsg { pub payment_collector: Option, // Whitelist address if any pub whitelist_address: Option, + pub num_tokens: u32, } #[cw_serde] @@ -30,7 +31,6 @@ pub struct CollectionDetails { pub id: String, pub extensible: bool, pub nsfw: bool, - pub num_tokens: u32, pub base_uri: String, pub uri: String, pub uri_hash: String, diff --git a/packages/open-edition-minter-types/Cargo.toml b/packages/open-edition-minter-types/Cargo.toml new file mode 100644 index 0000000..2d4f74c --- /dev/null +++ b/packages/open-edition-minter-types/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "open-edition-minter-types" +authors = ["Adnan Deniz Corlu "] +description = "Common types used by minter contracts" +version = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw721-base = { workspace = true, features = ["library"] } +cw-utils = { workspace = true } +thiserror = { workspace = true } +serde = { workspace = true } \ No newline at end of file diff --git a/packages/open-edition-minter-types/src/lib.rs b/packages/open-edition-minter-types/src/lib.rs new file mode 100644 index 0000000..81cdd11 --- /dev/null +++ b/packages/open-edition-minter-types/src/lib.rs @@ -0,0 +1,81 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Coin, Decimal, Timestamp, Uint128}; + +#[cw_serde] +pub struct InstantiateMsg { + pub collection_details: CollectionDetails, + pub admin: Option, + pub mint_price: Uint128, + pub mint_denom: String, + pub start_time: Timestamp, + pub end_time: Option, + pub token_limit: Option, + pub per_address_limit: u32, + pub royalty_ratio: String, + pub payment_collector: Option, + pub whitelist_address: Option, +} + +#[cw_serde] +pub struct CollectionDetails { + pub name: String, + pub description: String, + pub preview_uri: String, + pub schema: String, + pub symbol: String, + pub id: String, + pub extensible: bool, + pub nsfw: bool, + pub base_uri: String, + pub uri: String, + pub uri_hash: String, + pub data: String, +} + +#[cw_serde] +pub struct UserDetails { + pub minted_tokens: Vec, + pub total_minted_count: u32, +} + +impl Default for UserDetails { + fn default() -> Self { + UserDetails { + minted_tokens: Vec::new(), + total_minted_count: 0, + } + } +} + +#[cw_serde] +pub struct Token { + pub token_id: String, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(CollectionDetails)] + Collection {}, + #[returns(Config)] + Config {}, + #[returns(Vec)] + MintableTokens {}, + #[returns(UserDetails)] + MintedTokens { address: String }, + #[returns(u32)] + TotalTokens {}, +} + +#[cw_serde] +pub struct Config { + pub per_address_limit: u32, + pub payment_collector: Addr, + pub start_time: Timestamp, + pub end_time: Option, + pub mint_price: Coin, + pub royalty_ratio: Decimal, + pub admin: Addr, + pub whitelist_address: Option, + pub token_limit: Option, +} From 0e68e1e3d531cee83e373948aa4792042023acc9 Mon Sep 17 00:00:00 2001 From: Ninjatosba Date: Thu, 18 Jan 2024 19:50:19 +0300 Subject: [PATCH 02/11] implement queries --- .../open-edition-minter/src/contract.rs | 106 +++++++++--------- .../minters/open-edition-minter/src/error.rs | 8 ++ packages/open-edition-minter-types/src/lib.rs | 6 +- 3 files changed, 65 insertions(+), 55 deletions(-) diff --git a/contracts/minters/open-edition-minter/src/contract.rs b/contracts/minters/open-edition-minter/src/contract.rs index b209a65..0db04a2 100644 --- a/contracts/minters/open-edition-minter/src/contract.rs +++ b/contracts/minters/open-edition-minter/src/contract.rs @@ -4,11 +4,14 @@ use std::str::FromStr; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_json_binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Order, Response, - StdResult, Uint128, WasmMsg, + to_json_binary, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Order, + Response, StdResult, Uint128, WasmMsg, }; use cw_utils::{maybe_addr, must_pay, nonpayable}; -use open_edition_minter_types::{CollectionDetails, Config, InstantiateMsg, Token, UserDetails}; +use open_edition_minter_types::{ + CollectionDetails, Config, InstantiateMsg, QueryMsg, Token, UserDetails, +}; +use serde::de::value::Error; use crate::error::ContractError; use crate::msg::ExecuteMsg; @@ -506,52 +509,51 @@ pub fn execute_update_whitelist_address( Ok(res) } -// // Implement Queries -// #[cfg_attr(not(feature = "library"), entry_point)] -// pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { -// match msg { -// QueryMsg::Collection {} => to_json_binary(&query_collection(deps, env)?), -// QueryMsg::Config {} => to_json_binary(&query_config(deps, env)?), -// QueryMsg::MintableTokens {} => to_json_binary(&query_mintable_tokens(deps, env)?), -// QueryMsg::MintedTokens { address } => { -// to_json_binary(&query_minted_tokens(deps, env, address)?) -// } -// QueryMsg::TotalTokens {} => to_json_binary(&query_total_tokens(deps, env)?), -// } -// } - -// fn query_collection(deps: Deps, _env: Env) -> Result { -// let collection = COLLECTION.load(deps.storage)?; -// Ok(collection) -// } - -// fn query_config(deps: Deps, _env: Env) -> Result { -// let config = CONFIG.load(deps.storage)?; -// Ok(config) -// } - -// fn query_mintable_tokens(deps: Deps, _env: Env) -> Result, ContractError> { -// let mut mintable_tokens: Vec = Vec::new(); -// for item in MINTABLE_TOKENS.range(deps.storage, None, None, Order::Ascending) { -// let (_key, value) = item?; - -// // Add the (key, value) tuple to the vector -// mintable_tokens.push(value); -// } -// Ok(mintable_tokens) -// } - -// fn query_minted_tokens( -// deps: Deps, -// _env: Env, -// address: String, -// ) -> Result { -// let address = deps.api.addr_validate(&address)?; -// let minted_tokens = MINTED_TOKENS.load(deps.storage, address)?; -// Ok(minted_tokens) -// } - -// fn query_total_tokens(deps: Deps, _env: Env) -> Result { -// let total_tokens = TOTAL_TOKENS_REMAINING.load(deps.storage)?; -// Ok(total_tokens) -// } +// Implement Queries +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Collection {} => to_json_binary(&query_collection(deps, env)?), + QueryMsg::Config {} => to_json_binary(&query_config(deps, env)?), + QueryMsg::MintedTokens { address } => { + to_json_binary(&query_minted_tokens(deps, env, address)?) + } + QueryMsg::TotalMintedCount {} => to_json_binary(&query_total_tokens_minted(deps, env)?), + QueryMsg::TokensRemaining {} => to_json_binary(&query_tokens_remaining(deps, env)?), + } +} + +fn query_collection(deps: Deps, _env: Env) -> Result { + let collection = COLLECTION.load(deps.storage)?; + Ok(collection) +} + +fn query_config(deps: Deps, _env: Env) -> Result { + let config = CONFIG.load(deps.storage)?; + Ok(config) +} + +fn query_minted_tokens( + deps: Deps, + _env: Env, + address: String, +) -> Result { + let address = deps.api.addr_validate(&address)?; + let minted_tokens = MINTED_TOKENS.load(deps.storage, address)?; + Ok(minted_tokens) +} + +fn query_total_tokens_minted(deps: Deps, _env: Env) -> Result { + let total_tokens = MINTED_COUNT.load(deps.storage)?; + Ok(total_tokens) +} + +fn query_tokens_remaining(deps: Deps, _env: Env) -> Result { + let config = CONFIG.load(deps.storage)?; + if let Some(token_limit) = config.token_limit { + let total_tokens = MINTED_COUNT.load(deps.storage)?; + Ok(token_limit - total_tokens) + } else { + Err(ContractError::TokenLimitNotSet {}) + } +} diff --git a/contracts/minters/open-edition-minter/src/error.rs b/contracts/minters/open-edition-minter/src/error.rs index e8e4b4c..ff3b59e 100644 --- a/contracts/minters/open-edition-minter/src/error.rs +++ b/contracts/minters/open-edition-minter/src/error.rs @@ -86,4 +86,12 @@ pub enum ContractError { #[error("Token limit reached")] TokenLimitReached {}, + + #[error("Token limit not set")] + TokenLimitNotSet {}, +} +impl From for StdError { + fn from(err: ContractError) -> StdError { + StdError::generic_err(err.to_string()) + } } diff --git a/packages/open-edition-minter-types/src/lib.rs b/packages/open-edition-minter-types/src/lib.rs index 81cdd11..241907f 100644 --- a/packages/open-edition-minter-types/src/lib.rs +++ b/packages/open-edition-minter-types/src/lib.rs @@ -59,12 +59,12 @@ pub enum QueryMsg { Collection {}, #[returns(Config)] Config {}, - #[returns(Vec)] - MintableTokens {}, #[returns(UserDetails)] MintedTokens { address: String }, #[returns(u32)] - TotalTokens {}, + TotalMintedCount {}, + #[returns(u32)] + TokensRemaining {}, } #[cw_serde] From 326d6bfc8cf5014214e1d8d3a3569380900e8f96 Mon Sep 17 00:00:00 2001 From: Ninjatosba Date: Thu, 18 Jan 2024 20:05:12 +0300 Subject: [PATCH 03/11] add open edition minter factory --- Cargo.lock | 17 + .../open-edition-minter-factory/Cargo.toml | 54 ++ .../src/bin/schema.rs | 11 + .../src/contract.rs | 505 ++++++++++++++++++ .../open-edition-minter-factory/src/error.rs | 34 ++ .../open-edition-minter-factory/src/lib.rs | 5 + .../open-edition-minter-factory/src/msg.rs | 46 ++ .../open-edition-minter-factory/src/state.rs | 14 + .../open-edition-minter-factory/src/utils.rs | 114 ++++ .../open-edition-minter/src/contract.rs | 1 - 10 files changed, 800 insertions(+), 1 deletion(-) create mode 100644 contracts/factories/open-edition-minter-factory/Cargo.toml create mode 100644 contracts/factories/open-edition-minter-factory/src/bin/schema.rs create mode 100644 contracts/factories/open-edition-minter-factory/src/contract.rs create mode 100644 contracts/factories/open-edition-minter-factory/src/error.rs create mode 100644 contracts/factories/open-edition-minter-factory/src/lib.rs create mode 100644 contracts/factories/open-edition-minter-factory/src/msg.rs create mode 100644 contracts/factories/open-edition-minter-factory/src/state.rs create mode 100644 contracts/factories/open-edition-minter-factory/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 7ff42f2..df40d83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -784,6 +784,23 @@ dependencies = [ "whitelist-types", ] +[[package]] +name = "omniflix-open-edition-minter-factory" +version = "0.0.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.1", + "omniflix-std", + "open-edition-minter-types", + "serde", + "thiserror", +] + [[package]] name = "omniflix-round-whitelist" version = "0.0.1" diff --git a/contracts/factories/open-edition-minter-factory/Cargo.toml b/contracts/factories/open-edition-minter-factory/Cargo.toml new file mode 100644 index 0000000..c03368a --- /dev/null +++ b/contracts/factories/open-edition-minter-factory/Cargo.toml @@ -0,0 +1,54 @@ +[package] +authors = ["Adnan Deniz Corlu "] +name = "omniflix-open-edition-minter-factory" +version = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[[bin]] +name = "schema" +path = "src/bin/schema.rs" +doc = false + +[profile.release] +codegen-units = 1 +debug = false +debug-assertions = false +incremental = false +lto = true +opt-level = 3 +overflow-checks = true +panic = 'abort' +rpath = false + +[features] +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-storage = { workspace = true } +omniflix-std = { workspace = true } +thiserror = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-controllers = { workspace = true } +cw2 = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +serde = { workspace = true } +open-edition-minter-types = {workspace = true} + diff --git a/contracts/factories/open-edition-minter-factory/src/bin/schema.rs b/contracts/factories/open-edition-minter-factory/src/bin/schema.rs new file mode 100644 index 0000000..0c87022 --- /dev/null +++ b/contracts/factories/open-edition-minter-factory/src/bin/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; + +use omniflix_open_edition_minter_factory::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/factories/open-edition-minter-factory/src/contract.rs b/contracts/factories/open-edition-minter-factory/src/contract.rs new file mode 100644 index 0000000..ff180b2 --- /dev/null +++ b/contracts/factories/open-edition-minter-factory/src/contract.rs @@ -0,0 +1,505 @@ +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, ParamsResponse, QueryMsg}; +use crate::state::{Params, PARAMS}; +use crate::utils::check_payment; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, + StdResult, Uint128, WasmMsg, +}; +use cw_utils::maybe_addr; +use omniflix_std::types::omniflix::onft::v1beta1::OnftQuerier; +use open_edition_minter_types::InstantiateMsg as OpenEditionMinterInstantiateMsg; +use std::str::FromStr; +#[cfg(not(test))] +const CREATION_FEE: Uint128 = Uint128::new(0); +#[cfg(not(test))] +const CREATION_FEE_DENOM: &str = ""; + +#[cfg(test)] +const CREATION_FEE: Uint128 = Uint128::new(100_000_000); +#[cfg(test)] +const CREATION_FEE_DENOM: &str = "uflix"; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let admin = maybe_addr(deps.api, msg.admin)?.unwrap_or(info.sender); + let fee_collector_address = deps.api.addr_validate(&msg.fee_collector_address)?; + if msg.open_edition_minter_code_id == 0 { + return Err(ContractError::InvalidMinterCodeId {}); + } + let params = Params { + admin: admin.clone(), + allowed_minter_mint_denoms: msg.allowed_minter_mint_denoms, + fee_collector_address, + open_edition_minter_code_id: msg.open_edition_minter_code_id, + minter_creation_fee: msg.minter_creation_fee, + }; + PARAMS.save(deps.storage, ¶ms)?; + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::CreateMinter { msg } => create_minter(deps, env, info, msg), + ExecuteMsg::UpdateAdmin { admin } => update_params_admin(deps, env, info, admin), + ExecuteMsg::UpdateAllowedMinterMintDenoms { + allowed_minter_mint_denoms, + } => update_params_allowed_mint_denoms(deps, env, info, allowed_minter_mint_denoms), + ExecuteMsg::UpdateFeeCollectorAddress { + fee_collector_address, + } => update_params_fee_collector_address(deps, env, info, fee_collector_address), + ExecuteMsg::UpdateMinterCodeId { minter_code_id } => { + update_params_minter_code_id(deps, env, info, minter_code_id) + } + ExecuteMsg::UpdateMinterCreationFee { + minter_creation_fee, + } => update_params_minter_creation_fee(deps, env, info, minter_creation_fee), + } +} + +fn create_minter( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: OpenEditionMinterInstantiateMsg, +) -> Result { + let params = PARAMS.load(deps.storage)?; + let nft_creation_fee: Coin = if CREATION_FEE == Uint128::new(0) { + let onft_querier = OnftQuerier::new(&deps.querier); + let params = onft_querier.params()?; + let denom_creation_fee = params.params.unwrap().denom_creation_fee.unwrap(); + Coin { + amount: Uint128::from_str(&denom_creation_fee.amount)?, + denom: denom_creation_fee.denom, + } + } else { + Coin { + amount: CREATION_FEE, + denom: CREATION_FEE_DENOM.to_string(), + } + }; + check_payment( + &info.funds, + &[nft_creation_fee.clone(), params.minter_creation_fee.clone()], + )?; + + if !params.allowed_minter_mint_denoms.contains(&msg.mint_denom) { + return Err(ContractError::MintDenomNotAllowed {}); + } + + let msgs: Vec = vec![ + CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(params.admin.to_string()), + code_id: params.open_edition_minter_code_id, + msg: to_json_binary(&msg)?, + funds: vec![nft_creation_fee], + label: "omniflix-nft-minter".to_string(), + }), + CosmosMsg::Bank(BankMsg::Send { + amount: vec![params.minter_creation_fee], + to_address: params.fee_collector_address.to_string(), + }), + ]; + let res = Response::new() + .add_messages(msgs) + .add_attribute("action", "create_minter"); + Ok(res) +} + +fn update_params_admin( + deps: DepsMut, + _env: Env, + info: MessageInfo, + admin: String, +) -> Result { + let mut params = PARAMS.load(deps.storage)?; + if params.admin != info.sender { + return Err(ContractError::Unauthorized {}); + } + params.admin = deps.api.addr_validate(&admin)?; + PARAMS.save(deps.storage, ¶ms)?; + Ok(Response::default() + .add_attribute("action", "update_admin") + .add_attribute("new_admin", admin)) +} + +fn update_params_allowed_mint_denoms( + deps: DepsMut, + _env: Env, + info: MessageInfo, + allowed_mint_denoms: Vec, +) -> Result { + let mut params = PARAMS.load(deps.storage)?; + if params.admin != info.sender { + return Err(ContractError::Unauthorized {}); + } + params.allowed_minter_mint_denoms = allowed_mint_denoms.clone(); + PARAMS.save(deps.storage, ¶ms)?; + Ok(Response::default() + .add_attribute("action", "update_allowed_mint_denoms") + .add_attribute("new_allowed_mint_denoms", allowed_mint_denoms.join(","))) +} + +fn update_params_fee_collector_address( + deps: DepsMut, + _env: Env, + info: MessageInfo, + fee_collector_address: String, +) -> Result { + let mut params = PARAMS.load(deps.storage)?; + if params.admin != info.sender { + return Err(ContractError::Unauthorized {}); + } + params.fee_collector_address = deps.api.addr_validate(&fee_collector_address)?; + PARAMS.save(deps.storage, ¶ms)?; + Ok(Response::default() + .add_attribute("action", "update_fee_collector_address") + .add_attribute( + "new_fee_collector_address", + fee_collector_address.to_string(), + )) +} + +fn update_params_minter_code_id( + deps: DepsMut, + _env: Env, + info: MessageInfo, + minter_code_id: u64, +) -> Result { + let mut params = PARAMS.load(deps.storage)?; + if params.admin != info.sender { + return Err(ContractError::Unauthorized {}); + } + params.open_edition_minter_code_id = minter_code_id; + PARAMS.save(deps.storage, ¶ms)?; + Ok(Response::default() + .add_attribute("action", "update_minter_code_id") + .add_attribute("new_minter_code_id", minter_code_id.to_string())) +} + +fn update_params_minter_creation_fee( + deps: DepsMut, + _env: Env, + info: MessageInfo, + minter_creation_fee: Coin, +) -> Result { + let mut params = PARAMS.load(deps.storage)?; + if params.admin != info.sender { + return Err(ContractError::Unauthorized {}); + } + params.minter_creation_fee = minter_creation_fee.clone(); + PARAMS.save(deps.storage, ¶ms)?; + Ok(Response::default() + .add_attribute("action", "update_minter_creation_fee") + .add_attribute("new_minter_creation_fee", minter_creation_fee.to_string())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Params {} => to_json_binary(&query_params(deps)?), + } +} + +fn query_params(deps: Deps) -> StdResult { + let params = PARAMS.load(deps.storage)?; + Ok(ParamsResponse { params }) +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + Addr, Decimal, Timestamp, + }; + use open_edition_minter_types::CollectionDetails; + + #[test] + fn test_instantiate() { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg { + admin: None, + allowed_minter_mint_denoms: vec!["uusd".to_string(), "uflix".to_string()], + fee_collector_address: "fee_collector_address".to_string(), + open_edition_minter_code_id: 1, + minter_creation_fee: Coin { + amount: Uint128::new(100), + denom: "uusd".to_string(), + }, + }; + let info = mock_info("creator", &[]); + let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // query params + let params = query_params(deps.as_ref()).unwrap(); + assert_eq!( + params.params, + Params { + admin: Addr::unchecked("creator"), + allowed_minter_mint_denoms: vec!["uusd".to_string(), "uflix".to_string()], + fee_collector_address: Addr::unchecked("fee_collector_address"), + open_edition_minter_code_id: 1, + minter_creation_fee: Coin { + amount: Uint128::new(100), + denom: "uusd".to_string(), + }, + } + ); + } + + #[test] + fn test_execute_create_open_edition_minter() { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg { + admin: None, + allowed_minter_mint_denoms: vec!["uusd".to_string(), "uflix".to_string()], + fee_collector_address: "fee_collector_address".to_string(), + open_edition_minter_code_id: 1, + minter_creation_fee: Coin { + amount: Uint128::new(100), + denom: "uusd".to_string(), + }, + }; + let info = mock_info("creator", &[]); + let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + let collection_details = CollectionDetails { + name: "My Collection".to_string(), + description: "This is a collection of unique tokens.".to_string(), + preview_uri: "https://example.com/preview".to_string(), + schema: "https://example.com/schema".to_string(), + symbol: "SYM".to_string(), + id: "collection_id".to_string(), + extensible: true, + nsfw: false, + base_uri: "https://example.com/base".to_string(), + uri: "https://example.com/collection".to_string(), + uri_hash: "hash123".to_string(), + data: "Additional data for the collection".to_string(), + }; + // Non allowed mint denom + let msg = ExecuteMsg::CreateMinter { + msg: OpenEditionMinterInstantiateMsg { + admin: None, + whitelist_address: None, + mint_denom: "non_allowed".to_string(), + mint_price: Uint128::new(100), + start_time: Timestamp::from_seconds(0), + royalty_ratio: Decimal::percent(10).to_string(), + payment_collector: None, + per_address_limit: 3, + collection_details: collection_details.clone(), + end_time: None, + token_limit: None, + }, + }; + + let info = mock_info( + "creator", + &[ + Coin { + amount: Uint128::new(100_000_000), + denom: "uflix".to_string(), + }, + Coin { + amount: Uint128::new(100), + denom: "uusd".to_string(), + }, + ], + ); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!(res, ContractError::MintDenomNotAllowed {}); + // Send additional funds + let msg = ExecuteMsg::CreateMinter { + msg: OpenEditionMinterInstantiateMsg { + admin: None, + whitelist_address: None, + mint_denom: "uusd".to_string(), + mint_price: Uint128::new(100), + start_time: Timestamp::from_seconds(0), + royalty_ratio: Decimal::percent(10).to_string(), + payment_collector: None, + per_address_limit: 3, + collection_details: collection_details.clone(), + end_time: None, + token_limit: None, + }, + }; + + let info = mock_info( + "creator", + &[ + Coin { + amount: Uint128::new(100_000_000), + denom: "uflix".to_string(), + }, + Coin { + amount: Uint128::new(100), + denom: "uusd".to_string(), + }, + Coin { + amount: Uint128::new(100), + denom: "additional".to_string(), + }, + ], + ); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!( + res, + ContractError::IncorrectFunds { + expected: vec![ + Coin { + amount: Uint128::new(100_000_000), + denom: "uflix".to_string(), + }, + Coin { + amount: Uint128::new(100), + denom: "uusd".to_string(), + }, + ], + actual: vec![ + Coin { + amount: Uint128::new(100_000_000), + denom: "uflix".to_string(), + }, + Coin { + amount: Uint128::new(100), + denom: "uusd".to_string(), + }, + Coin { + amount: Uint128::new(100), + denom: "additional".to_string(), + }, + ], + } + ); + + // Missing funds + let msg = ExecuteMsg::CreateMinter { + msg: OpenEditionMinterInstantiateMsg { + admin: None, + whitelist_address: None, + mint_denom: "uusd".to_string(), + mint_price: Uint128::new(100), + start_time: Timestamp::from_seconds(0), + royalty_ratio: Decimal::percent(10).to_string(), + payment_collector: None, + per_address_limit: 3, + collection_details: collection_details.clone(), + end_time: None, + token_limit: None, + }, + }; + + let info = mock_info( + "creator", + &[Coin { + amount: Uint128::new(100_000_000), + denom: "uflix".to_string(), + }], + ); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + assert_eq!( + res, + ContractError::IncorrectFunds { + expected: vec![ + Coin { + amount: Uint128::new(100_000_000), + denom: "uflix".to_string(), + }, + Coin { + amount: Uint128::new(100), + denom: "uusd".to_string(), + }, + ], + actual: vec![Coin { + amount: Uint128::new(100_000_000), + denom: "uflix".to_string(), + },], + } + ); + + // Happy path + let msg = ExecuteMsg::CreateMinter { + msg: OpenEditionMinterInstantiateMsg { + admin: None, + whitelist_address: None, + mint_denom: "uusd".to_string(), + mint_price: Uint128::new(100), + start_time: Timestamp::from_seconds(0), + royalty_ratio: Decimal::percent(10).to_string(), + payment_collector: None, + per_address_limit: 3, + collection_details: collection_details.clone(), + end_time: None, + token_limit: None, + }, + }; + + let info = mock_info( + "creator", + &[ + Coin { + amount: Uint128::new(100_000_000), + denom: "uflix".to_string(), + }, + Coin { + amount: Uint128::new(100), + denom: "uusd".to_string(), + }, + ], + ); + let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + + assert_eq!(res.messages.len(), 2); + assert_eq!( + res.messages[0].msg, + CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some("creator".to_string()), + code_id: 1, + msg: to_json_binary(&OpenEditionMinterInstantiateMsg { + admin: None, + whitelist_address: None, + mint_denom: "uusd".to_string(), + mint_price: Uint128::new(100), + start_time: Timestamp::from_seconds(0), + royalty_ratio: Decimal::percent(10).to_string(), + payment_collector: None, + per_address_limit: 3, + collection_details: collection_details.clone(), + end_time: None, + token_limit: None, + }) + .unwrap(), + funds: vec![Coin { + amount: Uint128::new(100_000_000), + denom: "uflix".to_string(), + }], + label: "omniflix-nft-minter".to_string(), + }) + ); + assert_eq!( + res.messages[1].msg, + CosmosMsg::Bank(BankMsg::Send { + amount: vec![Coin { + amount: Uint128::new(100), + denom: "uusd".to_string(), + }], + to_address: "fee_collector_address".to_string(), + }) + ); + } +} diff --git a/contracts/factories/open-edition-minter-factory/src/error.rs b/contracts/factories/open-edition-minter-factory/src/error.rs new file mode 100644 index 0000000..b83b8d5 --- /dev/null +++ b/contracts/factories/open-edition-minter-factory/src/error.rs @@ -0,0 +1,34 @@ +use cosmwasm_std::{ + Coin, StdError, +}; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] + +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Invalid minter code id")] + InvalidMinterCodeId {}, + + #[error("Inncorrect funds")] + IncorrectFunds { + expected: Vec, + actual: Vec, + }, + #[error("Invalid Mint Denom")] + InvalidMintDenom {}, + + #[error("Mint denom not allowed")] + MintDenomNotAllowed {}, + + #[error("Missing creation fee")] + MissingCreationFee {}, + + #[error("Missing minter creation fee")] + MissingMinterCreationFee {}, +} diff --git a/contracts/factories/open-edition-minter-factory/src/lib.rs b/contracts/factories/open-edition-minter-factory/src/lib.rs new file mode 100644 index 0000000..8153c2f --- /dev/null +++ b/contracts/factories/open-edition-minter-factory/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; +pub mod utils; diff --git a/contracts/factories/open-edition-minter-factory/src/msg.rs b/contracts/factories/open-edition-minter-factory/src/msg.rs new file mode 100644 index 0000000..7846927 --- /dev/null +++ b/contracts/factories/open-edition-minter-factory/src/msg.rs @@ -0,0 +1,46 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Coin; +use open_edition_minter_types::InstantiateMsg as OpenEditionMinterInstantiateMsg; + +use crate::state::Params; +#[cw_serde] +pub struct InstantiateMsg { + pub admin: Option, + pub allowed_minter_mint_denoms: Vec, + pub fee_collector_address: String, + pub open_edition_minter_code_id: u64, + pub minter_creation_fee: Coin, +} +#[cw_serde] +pub enum ExecuteMsg { + CreateMinter { + msg: OpenEditionMinterInstantiateMsg, + }, + UpdateAdmin { + admin: String, + }, + UpdateFeeCollectorAddress { + fee_collector_address: String, + }, + UpdateMinterCreationFee { + minter_creation_fee: Coin, + }, + UpdateAllowedMinterMintDenoms { + allowed_minter_mint_denoms: Vec, + }, + UpdateMinterCodeId { + minter_code_id: u64, + }, +} + +#[cw_serde] +pub struct ParamsResponse { + pub params: Params, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(ParamsResponse)] + Params {}, +} diff --git a/contracts/factories/open-edition-minter-factory/src/state.rs b/contracts/factories/open-edition-minter-factory/src/state.rs new file mode 100644 index 0000000..86c4e99 --- /dev/null +++ b/contracts/factories/open-edition-minter-factory/src/state.rs @@ -0,0 +1,14 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Coin}; +use cw_storage_plus::Item; + +#[cw_serde] +pub struct Params { + pub minter_creation_fee: Coin, + pub fee_collector_address: Addr, + pub open_edition_minter_code_id: u64, + pub allowed_minter_mint_denoms: Vec, + pub admin: Addr, +} + +pub const PARAMS: Item = Item::new("params"); diff --git a/contracts/factories/open-edition-minter-factory/src/utils.rs b/contracts/factories/open-edition-minter-factory/src/utils.rs new file mode 100644 index 0000000..833f705 --- /dev/null +++ b/contracts/factories/open-edition-minter-factory/src/utils.rs @@ -0,0 +1,114 @@ +use cosmwasm_std::Coin; + +use crate::error::ContractError; + +pub fn check_payment(sent_funds: &[Coin], expected_funds: &[Coin]) -> Result<(), ContractError> { + // Check length + if sent_funds.len() > expected_funds.len() { + return Err(ContractError::IncorrectFunds { + expected: expected_funds.to_vec(), + actual: sent_funds.to_vec(), + }); + } + + let mut mut_sent_funds = sent_funds.to_vec(); // Create a mutable copy + + for expected in expected_funds { + if let Some(sent_index) = mut_sent_funds + .iter() + .position(|sent| expected.denom == sent.denom) + { + let sent = &mut mut_sent_funds[sent_index]; + if expected.amount > sent.amount { + return Err(ContractError::IncorrectFunds { + expected: expected_funds.to_vec(), + actual: sent_funds.to_vec(), + }); + } else { + sent.amount = sent.amount.checked_sub(expected.amount).unwrap(); + } + } else { + return Err(ContractError::IncorrectFunds { + expected: expected_funds.to_vec(), + actual: sent_funds.to_vec(), + }); + } + } + + Ok(()) +} + +// Test check_payment +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::coin; + + #[test] + fn test_check_payment() { + let sent_funds = vec![coin(100, "uluna"), coin(100, "uusd")]; + let expected_funds = vec![coin(100, "uluna"), coin(100, "uusd")]; + let res = check_payment(&sent_funds, &expected_funds); + assert!(res.is_ok()); + + let sent_funds = vec![coin(100, "uluna"), coin(100, "uusd")]; + let expected_funds = vec![coin(100, "uluna")]; + let res = check_payment(&sent_funds, &expected_funds); + assert!(res.is_err()); + + let sent_funds = vec![coin(100, "uluna")]; + let expected_funds = vec![coin(100, "uluna"), coin(100, "uusd")]; + let res = check_payment(&sent_funds, &expected_funds); + assert!(res.is_err()); + + let sent_funds = vec![coin(100, "uluna"), coin(100, "uusd")]; + let expected_funds = vec![coin(100, "uluna"), coin(200, "uusd")]; + let res = check_payment(&sent_funds, &expected_funds); + assert!(res.is_err()); + + let sent_funds = vec![coin(300, "uluna")]; + let expected_funds = vec![coin(100, "uluna"), coin(200, "uluna")]; + let res = check_payment(&sent_funds, &expected_funds); + assert!(res.is_ok()); + + let sent_funds = vec![coin(300 - 1, "uluna")]; + let expected_funds = vec![coin(100, "uluna"), coin(200, "uluna")]; + let res = check_payment(&sent_funds, &expected_funds); + assert!(res.is_err()); + + let sent_funds = vec![coin(300, "uluna"), coin(100, "uusd")]; + let expected_funds = vec![coin(300, "uluna"), coin(200, "uatom")]; + let res = check_payment(&sent_funds, &expected_funds); + assert!(res.is_err()); + + let sent_funds = vec![coin(1100, "uluna")]; + let expected_funds = vec![ + coin(100, "uluna"), + coin(200, "uluna"), + coin(300, "uluna"), + coin(500, "uluna"), + ]; + let res = check_payment(&sent_funds, &expected_funds); + assert!(res.is_ok()); + + let sent_funds = vec![coin(1100 + 1, "uluna")]; + let expected_funds = vec![ + coin(100, "uluna"), + coin(200, "uluna"), + coin(300, "uluna"), + coin(500, "uluna"), + ]; + let res = check_payment(&sent_funds, &expected_funds); + assert!(res.is_ok()); + + let sent_funds = vec![coin(1100 - 1, "uluna")]; + let expected_funds = vec![ + coin(100, "uluna"), + coin(200, "uluna"), + coin(300, "uluna"), + coin(500, "uluna"), + ]; + let res = check_payment(&sent_funds, &expected_funds); + assert!(res.is_err()); + } +} diff --git a/contracts/minters/open-edition-minter/src/contract.rs b/contracts/minters/open-edition-minter/src/contract.rs index 0db04a2..c8f2789 100644 --- a/contracts/minters/open-edition-minter/src/contract.rs +++ b/contracts/minters/open-edition-minter/src/contract.rs @@ -11,7 +11,6 @@ use cw_utils::{maybe_addr, must_pay, nonpayable}; use open_edition_minter_types::{ CollectionDetails, Config, InstantiateMsg, QueryMsg, Token, UserDetails, }; -use serde::de::value::Error; use crate::error::ContractError; use crate::msg::ExecuteMsg; From dbe0d7021b5c2db9969a68a06f6c18bda24ac2f2 Mon Sep 17 00:00:00 2001 From: Ninjatosba Date: Thu, 18 Jan 2024 20:10:16 +0300 Subject: [PATCH 04/11] save minted token to user details --- contracts/minters/minter/src/contract.rs | 3 --- .../open-edition-minter/src/contract.rs | 19 +++++++++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/contracts/minters/minter/src/contract.rs b/contracts/minters/minter/src/contract.rs index ebe8c3e..8cffd14 100644 --- a/contracts/minters/minter/src/contract.rs +++ b/contracts/minters/minter/src/contract.rs @@ -84,12 +84,10 @@ pub fn instantiate( if msg.per_address_limit == 0 { return Err(ContractError::PerAddressLimitZero {}); } - // Check num_tokens if msg.num_tokens == 0 { return Err(ContractError::InvalidNumTokens {}); } - // Check start time if msg.start_time < env.block.time { return Err(ContractError::InvalidStartTime {}); @@ -562,7 +560,6 @@ pub fn execute_randomize_list( info: MessageInfo, ) -> Result { // Check if sender is admin - let collection = COLLECTION.load(deps.storage)?; let config = CONFIG.load(deps.storage)?; // This should be available for everyone but then this could be abused if info.sender != config.admin { diff --git a/contracts/minters/open-edition-minter/src/contract.rs b/contracts/minters/open-edition-minter/src/contract.rs index c8f2789..c51806c 100644 --- a/contracts/minters/open-edition-minter/src/contract.rs +++ b/contracts/minters/open-edition-minter/src/contract.rs @@ -4,8 +4,8 @@ use std::str::FromStr; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_json_binary, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Order, - Response, StdResult, Uint128, WasmMsg, + to_json_binary, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Response, + StdResult, Uint128, WasmMsg, }; use cw_utils::{maybe_addr, must_pay, nonpayable}; use open_edition_minter_types::{ @@ -228,6 +228,11 @@ pub fn execute_mint(deps: DepsMut, env: Env, info: MessageInfo) -> Result config.per_address_limit { return Err(ContractError::AddressReachedMintLimit {}); } + let token_id = last_token_id(deps.storage) + 1; + user_details.minted_tokens.push(Token { + token_id: token_id.to_string(), + }); + MINTED_TOKENS.save(deps.storage, info.sender.clone(), &user_details)?; let mut mint_price = config.mint_price; // Check if minting is started @@ -297,7 +302,6 @@ pub fn execute_mint(deps: DepsMut, env: Env, info: MessageInfo) -> Result Date: Thu, 18 Jan 2024 20:11:54 +0300 Subject: [PATCH 05/11] style --- contracts/minters/open-edition-minter/src/error.rs | 4 +--- contracts/minters/open-edition-minter/src/state.rs | 2 +- contracts/whitelists/round-whitelist/src/state.rs | 2 +- integration-tests/src/test_round_whitelist_creation.rs | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/contracts/minters/open-edition-minter/src/error.rs b/contracts/minters/open-edition-minter/src/error.rs index ff3b59e..ea1c439 100644 --- a/contracts/minters/open-edition-minter/src/error.rs +++ b/contracts/minters/open-edition-minter/src/error.rs @@ -1,6 +1,4 @@ -use std::convert::Infallible; - -use cosmwasm_std::{CheckedFromRatioError, ConversionOverflowError, StdError, Timestamp, Uint128}; +use cosmwasm_std::{StdError, Timestamp, Uint128}; use cw_utils::PaymentError; use thiserror::Error; diff --git a/contracts/minters/open-edition-minter/src/state.rs b/contracts/minters/open-edition-minter/src/state.rs index bcc5261..844d17c 100644 --- a/contracts/minters/open-edition-minter/src/state.rs +++ b/contracts/minters/open-edition-minter/src/state.rs @@ -3,7 +3,7 @@ use std::u32; use cosmwasm_std::{Addr, Storage}; use cw_storage_plus::{Item, Map}; -use open_edition_minter_types::{CollectionDetails, Config, Token, UserDetails}; +use open_edition_minter_types::{CollectionDetails, Config, UserDetails}; pub const CONFIG: Item = Item::new("config"); pub const COLLECTION: Item = Item::new("collection"); diff --git a/contracts/whitelists/round-whitelist/src/state.rs b/contracts/whitelists/round-whitelist/src/state.rs index a3f2fff..5f87734 100644 --- a/contracts/whitelists/round-whitelist/src/state.rs +++ b/contracts/whitelists/round-whitelist/src/state.rs @@ -1,6 +1,6 @@ use crate::round::RoundMethods; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Order, StdError, StdResult, Storage, Timestamp}; +use cosmwasm_std::{Addr, Order, StdResult, Storage, Timestamp}; use cw_storage_plus::{Item, Map}; use crate::error::ContractError; diff --git a/integration-tests/src/test_round_whitelist_creation.rs b/integration-tests/src/test_round_whitelist_creation.rs index 26d55aa..c55720f 100644 --- a/integration-tests/src/test_round_whitelist_creation.rs +++ b/integration-tests/src/test_round_whitelist_creation.rs @@ -344,7 +344,7 @@ mod test_round_whitelist_creation { let error = res.downcast_ref::().unwrap(); assert_eq!(error, &RoundWhitelistContractError::RoundAlreadyStarted {}); // Remove round - let res = app + let _res = app .execute_contract( admin.clone(), Addr::unchecked(round_whitelist_address.clone()), From 26feabd453ff359f4ac2a3bf6c9d29fc45a06ddb Mon Sep 17 00:00:00 2001 From: Ninjatosba Date: Thu, 18 Jan 2024 20:15:37 +0300 Subject: [PATCH 06/11] add factory check for minter --- Cargo.lock | 1 + contracts/minters/open-edition-minter/Cargo.toml | 2 ++ contracts/minters/open-edition-minter/src/contract.rs | 10 +++++++--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df40d83..0f71934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -774,6 +774,7 @@ dependencies = [ "cw-utils 1.0.3", "cw2 1.1.1", "minter-types", + "omniflix-open-edition-minter-factory", "omniflix-round-whitelist", "omniflix-std", "open-edition-minter-types", diff --git a/contracts/minters/open-edition-minter/Cargo.toml b/contracts/minters/open-edition-minter/Cargo.toml index 3defcd4..0a13cf9 100644 --- a/contracts/minters/open-edition-minter/Cargo.toml +++ b/contracts/minters/open-edition-minter/Cargo.toml @@ -55,4 +55,6 @@ minter-types={ workspace = true } sha2 = { version = "0.10.2", default-features = false } whitelist-types ={ workspace = true } open-edition-minter-types = {workspace=true} +omniflix-open-edition-minter-factory = {path = "../../factories/open-edition-minter-factory"} omniflix-round-whitelist = {path="../../whitelists/round-whitelist"} + diff --git a/contracts/minters/open-edition-minter/src/contract.rs b/contracts/minters/open-edition-minter/src/contract.rs index c51806c..8fc2aa9 100644 --- a/contracts/minters/open-edition-minter/src/contract.rs +++ b/contracts/minters/open-edition-minter/src/contract.rs @@ -16,6 +16,9 @@ use crate::error::ContractError; use crate::msg::ExecuteMsg; use crate::state::{last_token_id, COLLECTION, CONFIG, MINTED_COUNT, MINTED_TOKENS}; use cw2::set_contract_version; +use omniflix_open_edition_minter_factory::msg::{ + ParamsResponse, QueryMsg as OpenEditionMinterFactoryQueryMsg, +}; use omniflix_round_whitelist::msg::ExecuteMsg as RoundWhitelistExecuteMsg; use omniflix_std::types::omniflix::onft::v1beta1::{ Metadata, MsgCreateDenom, MsgMintOnft, OnftQuerier, @@ -51,9 +54,10 @@ pub fn instantiate( set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; // Query factory params of instantiator // If the instantiator is not a our factory then we wont be able to parse the response - // let _factory_params: ParamsResponse = deps - // .querier - // .query_wasm_smart(info.sender.clone().into_string(), &QueryFactoryParams {})?; + let _factory_params: ParamsResponse = deps.querier.query_wasm_smart( + info.sender.clone().into_string(), + &OpenEditionMinterFactoryQueryMsg::Params {}, + )?; // This field is implemented only for testing purposes let creation_fee_amount = if CREATION_FEE == Uint128::new(0) { From cc319c79e0a68446d9e7c4fead092ebdc7bab8b4 Mon Sep 17 00:00:00 2001 From: Ninjatosba Date: Mon, 22 Jan 2024 15:10:12 +0300 Subject: [PATCH 07/11] change instantiate msg structure --- Cargo.lock | 2 + .../factories/minter-factory/src/contract.rs | 132 +++++++++-------- contracts/factories/minter-factory/src/msg.rs | 28 +++- .../open-edition-minter-factory/Cargo.toml | 1 + .../src/contract.rs | 136 ++++++++++-------- .../open-edition-minter-factory/src/msg.rs | 23 ++- contracts/minters/minter/src/bin/schema.rs | 6 +- contracts/minters/minter/src/contract.rs | 41 +++--- .../open-edition-minter/src/bin/schema.rs | 6 +- .../open-edition-minter/src/contract.rs | 41 +++--- .../minters/open-edition-minter/src/state.rs | 2 +- integration-tests/src/test_minter_creation.rs | 10 +- integration-tests/src/test_minting.rs | 4 +- .../src/test_open_edition_minter.rs | 62 ++++++++ integration-tests/src/utils.rs | 35 ++--- packages/minter-types/src/lib.rs | 27 +--- packages/open-edition-minter-types/Cargo.toml | 3 +- packages/open-edition-minter-types/src/lib.rs | 66 +-------- 18 files changed, 345 insertions(+), 280 deletions(-) create mode 100644 integration-tests/src/test_open_edition_minter.rs diff --git a/Cargo.lock b/Cargo.lock index 0f71934..3bc291d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -796,6 +796,7 @@ dependencies = [ "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.1", + "minter-types", "omniflix-std", "open-edition-minter-types", "serde", @@ -912,6 +913,7 @@ dependencies = [ "cosmwasm-std", "cw-utils 1.0.3", "cw721-base 0.18.0", + "minter-types", "serde", "thiserror", ] diff --git a/contracts/factories/minter-factory/src/contract.rs b/contracts/factories/minter-factory/src/contract.rs index 0dc4d34..7d8cef3 100644 --- a/contracts/factories/minter-factory/src/contract.rs +++ b/contracts/factories/minter-factory/src/contract.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use crate::error::ContractError; -use crate::msg::{ExecuteMsg, InstantiateMsg, ParamsResponse, QueryMsg}; +use crate::msg::{CreateMinterMsg, ExecuteMsg, InstantiateMsg, ParamsResponse, QueryMsg}; use crate::state::{Params, PARAMS}; use crate::utils::check_payment; #[cfg(not(feature = "library"))] @@ -11,7 +11,6 @@ use cosmwasm_std::{ StdResult, Uint128, WasmMsg, }; use cw_utils::maybe_addr; -use minter_types::InstantiateMsg as MinterInstantiateMsg; use omniflix_std::types::omniflix::onft::v1beta1::OnftQuerier; #[cfg(not(test))] const CREATION_FEE: Uint128 = Uint128::new(0); @@ -75,7 +74,7 @@ fn create_minter( deps: DepsMut, _env: Env, info: MessageInfo, - msg: MinterInstantiateMsg, + msg: CreateMinterMsg, ) -> Result { let params = PARAMS.load(deps.storage)?; let nft_creation_fee: Coin = if CREATION_FEE == Uint128::new(0) { @@ -97,7 +96,10 @@ fn create_minter( &[nft_creation_fee.clone(), params.minter_creation_fee.clone()], )?; - if !params.allowed_minter_mint_denoms.contains(&msg.mint_denom) { + if !params + .allowed_minter_mint_denoms + .contains(&msg.init.mint_denom) + { return Err(ContractError::MintDenomNotAllowed {}); } @@ -222,6 +224,8 @@ fn query_params(deps: Deps) -> StdResult { #[cfg(test)] mod tests { + use crate::msg::MinterInitExtention; + use super::*; use cosmwasm_std::{ testing::{mock_dependencies, mock_env, mock_info}, @@ -293,18 +297,20 @@ mod tests { }; // Non allowed mint denom let msg = ExecuteMsg::CreateMinter { - msg: MinterInstantiateMsg { - admin: None, - whitelist_address: None, - mint_denom: "non_allowed".to_string(), - mint_price: Uint128::new(100), - start_time: Timestamp::from_seconds(0), - royalty_ratio: Decimal::percent(10).to_string(), - payment_collector: None, - per_address_limit: 3, + msg: CreateMinterMsg { collection_details: collection_details.clone(), - end_time: None, - num_tokens: 100, + init: MinterInitExtention { + admin: None, + whitelist_address: None, + mint_denom: "non_allowed".to_string(), + mint_price: Uint128::new(100), + start_time: Timestamp::from_seconds(0), + royalty_ratio: Decimal::percent(10).to_string(), + payment_collector: None, + per_address_limit: 3, + end_time: None, + num_tokens: 100, + }, }, }; @@ -325,18 +331,20 @@ mod tests { assert_eq!(res, ContractError::MintDenomNotAllowed {}); // Send additional funds let msg = ExecuteMsg::CreateMinter { - msg: MinterInstantiateMsg { - admin: None, - whitelist_address: None, - mint_denom: "uusd".to_string(), - mint_price: Uint128::new(100), - start_time: Timestamp::from_seconds(0), - royalty_ratio: Decimal::percent(10).to_string(), - payment_collector: None, - per_address_limit: 3, + msg: CreateMinterMsg { collection_details: collection_details.clone(), - end_time: None, - num_tokens: 100, + init: MinterInitExtention { + admin: None, + whitelist_address: None, + mint_denom: "uusd".to_string(), + mint_price: Uint128::new(100), + start_time: Timestamp::from_seconds(0), + royalty_ratio: Decimal::percent(10).to_string(), + payment_collector: None, + per_address_limit: 3, + end_time: None, + num_tokens: 100, + }, }, }; @@ -390,18 +398,20 @@ mod tests { // Missing funds let msg = ExecuteMsg::CreateMinter { - msg: MinterInstantiateMsg { - admin: None, - whitelist_address: None, - mint_denom: "uusd".to_string(), - mint_price: Uint128::new(100), - start_time: Timestamp::from_seconds(0), - royalty_ratio: Decimal::percent(10).to_string(), - payment_collector: None, - per_address_limit: 3, + msg: CreateMinterMsg { collection_details: collection_details.clone(), - end_time: None, - num_tokens: 100, + init: MinterInitExtention { + admin: None, + whitelist_address: None, + mint_denom: "uusd".to_string(), + mint_price: Uint128::new(100), + start_time: Timestamp::from_seconds(0), + royalty_ratio: Decimal::percent(10).to_string(), + payment_collector: None, + per_address_limit: 3, + end_time: None, + num_tokens: 100, + }, }, }; @@ -435,18 +445,20 @@ mod tests { // Happy path let msg = ExecuteMsg::CreateMinter { - msg: MinterInstantiateMsg { - admin: None, - whitelist_address: None, - mint_denom: "uusd".to_string(), - mint_price: Uint128::new(100), - start_time: Timestamp::from_seconds(0), - royalty_ratio: Decimal::percent(10).to_string(), - payment_collector: None, - per_address_limit: 3, + msg: CreateMinterMsg { collection_details: collection_details.clone(), - end_time: None, - num_tokens: 100, + init: MinterInitExtention { + admin: None, + whitelist_address: None, + mint_denom: "uusd".to_string(), + mint_price: Uint128::new(100), + start_time: Timestamp::from_seconds(0), + royalty_ratio: Decimal::percent(10).to_string(), + payment_collector: None, + per_address_limit: 3, + end_time: None, + num_tokens: 100, + }, }, }; @@ -471,18 +483,20 @@ mod tests { CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some("creator".to_string()), code_id: 1, - msg: to_json_binary(&MinterInstantiateMsg { - admin: None, - whitelist_address: None, - mint_denom: "uusd".to_string(), - mint_price: Uint128::new(100), - start_time: Timestamp::from_seconds(0), - royalty_ratio: Decimal::percent(10).to_string(), - payment_collector: None, - per_address_limit: 3, + msg: to_json_binary(&CreateMinterMsg { collection_details: collection_details.clone(), - end_time: None, - num_tokens: 100, + init: MinterInitExtention { + admin: None, + whitelist_address: None, + mint_denom: "uusd".to_string(), + mint_price: Uint128::new(100), + start_time: Timestamp::from_seconds(0), + royalty_ratio: Decimal::percent(10).to_string(), + payment_collector: None, + per_address_limit: 3, + end_time: None, + num_tokens: 100, + }, }) .unwrap(), funds: vec![Coin { diff --git a/contracts/factories/minter-factory/src/msg.rs b/contracts/factories/minter-factory/src/msg.rs index 8401b59..a704148 100644 --- a/contracts/factories/minter-factory/src/msg.rs +++ b/contracts/factories/minter-factory/src/msg.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Coin; -use minter_types::InstantiateMsg as MinterInstantiateMsg; +use cosmwasm_std::{Coin, Timestamp, Uint128}; +use minter_types::MinterInstantiateMsg; use crate::state::Params; #[cw_serde] @@ -11,10 +11,32 @@ pub struct InstantiateMsg { pub minter_code_id: u64, pub minter_creation_fee: Coin, } + +#[cw_serde] +pub struct MinterInitExtention { + pub admin: Option, + pub mint_price: Uint128, + // Factory sould check denom against the params if denoms is valid and whitelisted. + pub mint_denom: String, + // Public minting start time + pub start_time: Timestamp, + pub end_time: Option, + pub per_address_limit: u32, + // We expect user to send a string between 0 and 1 + // FE "0.1" + pub royalty_ratio: String, + pub payment_collector: Option, + // Whitelist address if any + pub whitelist_address: Option, + pub num_tokens: u32, +} + +pub type CreateMinterMsg = MinterInstantiateMsg; + #[cw_serde] pub enum ExecuteMsg { CreateMinter { - msg: MinterInstantiateMsg, + msg: CreateMinterMsg, }, UpdateAdmin { admin: String, diff --git a/contracts/factories/open-edition-minter-factory/Cargo.toml b/contracts/factories/open-edition-minter-factory/Cargo.toml index c03368a..7ac6f98 100644 --- a/contracts/factories/open-edition-minter-factory/Cargo.toml +++ b/contracts/factories/open-edition-minter-factory/Cargo.toml @@ -51,4 +51,5 @@ cw-storage-plus = { workspace = true } cw-utils = { workspace = true } serde = { workspace = true } open-edition-minter-types = {workspace = true} +minter-types = {workspace = true} diff --git a/contracts/factories/open-edition-minter-factory/src/contract.rs b/contracts/factories/open-edition-minter-factory/src/contract.rs index ff180b2..a13b9ab 100644 --- a/contracts/factories/open-edition-minter-factory/src/contract.rs +++ b/contracts/factories/open-edition-minter-factory/src/contract.rs @@ -1,5 +1,7 @@ use crate::error::ContractError; -use crate::msg::{ExecuteMsg, InstantiateMsg, ParamsResponse, QueryMsg}; +use crate::msg::{ + ExecuteMsg, InstantiateMsg, OpenEditionMinterCreateMsg, ParamsResponse, QueryMsg, +}; use crate::state::{Params, PARAMS}; use crate::utils::check_payment; #[cfg(not(feature = "library"))] @@ -9,8 +11,8 @@ use cosmwasm_std::{ StdResult, Uint128, WasmMsg, }; use cw_utils::maybe_addr; +use minter_types::CollectionDetails; use omniflix_std::types::omniflix::onft::v1beta1::OnftQuerier; -use open_edition_minter_types::InstantiateMsg as OpenEditionMinterInstantiateMsg; use std::str::FromStr; #[cfg(not(test))] const CREATION_FEE: Uint128 = Uint128::new(0); @@ -74,7 +76,7 @@ fn create_minter( deps: DepsMut, _env: Env, info: MessageInfo, - msg: OpenEditionMinterInstantiateMsg, + msg: OpenEditionMinterCreateMsg, ) -> Result { let params = PARAMS.load(deps.storage)?; let nft_creation_fee: Coin = if CREATION_FEE == Uint128::new(0) { @@ -96,7 +98,10 @@ fn create_minter( &[nft_creation_fee.clone(), params.minter_creation_fee.clone()], )?; - if !params.allowed_minter_mint_denoms.contains(&msg.mint_denom) { + if !params + .allowed_minter_mint_denoms + .contains(&msg.init.mint_denom) + { return Err(ContractError::MintDenomNotAllowed {}); } @@ -221,12 +226,13 @@ fn query_params(deps: Deps) -> StdResult { #[cfg(test)] mod tests { + use crate::msg::OpenEditionMinterInitExtention; + use super::*; use cosmwasm_std::{ testing::{mock_dependencies, mock_env, mock_info}, Addr, Decimal, Timestamp, }; - use open_edition_minter_types::CollectionDetails; #[test] fn test_instantiate() { @@ -292,18 +298,20 @@ mod tests { }; // Non allowed mint denom let msg = ExecuteMsg::CreateMinter { - msg: OpenEditionMinterInstantiateMsg { - admin: None, - whitelist_address: None, - mint_denom: "non_allowed".to_string(), - mint_price: Uint128::new(100), - start_time: Timestamp::from_seconds(0), - royalty_ratio: Decimal::percent(10).to_string(), - payment_collector: None, - per_address_limit: 3, + msg: OpenEditionMinterCreateMsg { collection_details: collection_details.clone(), - end_time: None, - token_limit: None, + init: OpenEditionMinterInitExtention { + admin: None, + whitelist_address: None, + mint_denom: "non_allowed_denom".to_string(), + mint_price: Uint128::new(100), + start_time: Timestamp::from_seconds(0), + royalty_ratio: Decimal::percent(10).to_string(), + payment_collector: None, + per_address_limit: 3, + end_time: None, + token_limit: None, + }, }, }; @@ -324,18 +332,20 @@ mod tests { assert_eq!(res, ContractError::MintDenomNotAllowed {}); // Send additional funds let msg = ExecuteMsg::CreateMinter { - msg: OpenEditionMinterInstantiateMsg { - admin: None, - whitelist_address: None, - mint_denom: "uusd".to_string(), - mint_price: Uint128::new(100), - start_time: Timestamp::from_seconds(0), - royalty_ratio: Decimal::percent(10).to_string(), - payment_collector: None, - per_address_limit: 3, + msg: OpenEditionMinterCreateMsg { collection_details: collection_details.clone(), - end_time: None, - token_limit: None, + init: OpenEditionMinterInitExtention { + admin: None, + whitelist_address: None, + mint_denom: "uusd".to_string(), + mint_price: Uint128::new(100), + start_time: Timestamp::from_seconds(0), + royalty_ratio: Decimal::percent(10).to_string(), + payment_collector: None, + per_address_limit: 3, + end_time: None, + token_limit: None, + }, }, }; @@ -389,18 +399,20 @@ mod tests { // Missing funds let msg = ExecuteMsg::CreateMinter { - msg: OpenEditionMinterInstantiateMsg { - admin: None, - whitelist_address: None, - mint_denom: "uusd".to_string(), - mint_price: Uint128::new(100), - start_time: Timestamp::from_seconds(0), - royalty_ratio: Decimal::percent(10).to_string(), - payment_collector: None, - per_address_limit: 3, + msg: OpenEditionMinterCreateMsg { collection_details: collection_details.clone(), - end_time: None, - token_limit: None, + init: OpenEditionMinterInitExtention { + admin: None, + whitelist_address: None, + mint_denom: "uusd".to_string(), + mint_price: Uint128::new(100), + start_time: Timestamp::from_seconds(0), + royalty_ratio: Decimal::percent(10).to_string(), + payment_collector: None, + per_address_limit: 3, + end_time: None, + token_limit: None, + }, }, }; @@ -434,18 +446,20 @@ mod tests { // Happy path let msg = ExecuteMsg::CreateMinter { - msg: OpenEditionMinterInstantiateMsg { - admin: None, - whitelist_address: None, - mint_denom: "uusd".to_string(), - mint_price: Uint128::new(100), - start_time: Timestamp::from_seconds(0), - royalty_ratio: Decimal::percent(10).to_string(), - payment_collector: None, - per_address_limit: 3, + msg: OpenEditionMinterCreateMsg { collection_details: collection_details.clone(), - end_time: None, - token_limit: None, + init: OpenEditionMinterInitExtention { + admin: None, + whitelist_address: None, + mint_denom: "uusd".to_string(), + mint_price: Uint128::new(100), + start_time: Timestamp::from_seconds(0), + royalty_ratio: Decimal::percent(10).to_string(), + payment_collector: None, + per_address_limit: 3, + end_time: None, + token_limit: None, + }, }, }; @@ -470,18 +484,20 @@ mod tests { CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some("creator".to_string()), code_id: 1, - msg: to_json_binary(&OpenEditionMinterInstantiateMsg { - admin: None, - whitelist_address: None, - mint_denom: "uusd".to_string(), - mint_price: Uint128::new(100), - start_time: Timestamp::from_seconds(0), - royalty_ratio: Decimal::percent(10).to_string(), - payment_collector: None, - per_address_limit: 3, + msg: to_json_binary(&OpenEditionMinterCreateMsg { collection_details: collection_details.clone(), - end_time: None, - token_limit: None, + init: OpenEditionMinterInitExtention { + admin: None, + whitelist_address: None, + mint_denom: "uusd".to_string(), + mint_price: Uint128::new(100), + start_time: Timestamp::from_seconds(0), + royalty_ratio: Decimal::percent(10).to_string(), + payment_collector: None, + per_address_limit: 3, + end_time: None, + token_limit: None, + }, }) .unwrap(), funds: vec![Coin { diff --git a/contracts/factories/open-edition-minter-factory/src/msg.rs b/contracts/factories/open-edition-minter-factory/src/msg.rs index 7846927..512fb3c 100644 --- a/contracts/factories/open-edition-minter-factory/src/msg.rs +++ b/contracts/factories/open-edition-minter-factory/src/msg.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Coin; -use open_edition_minter_types::InstantiateMsg as OpenEditionMinterInstantiateMsg; +use cosmwasm_std::{Coin, Timestamp, Uint128}; +use minter_types::{CollectionDetails, MinterInstantiateMsg}; use crate::state::Params; #[cw_serde] @@ -11,10 +11,27 @@ pub struct InstantiateMsg { pub open_edition_minter_code_id: u64, pub minter_creation_fee: Coin, } + +#[cw_serde] +pub struct OpenEditionMinterInitExtention { + pub admin: Option, + pub mint_price: Uint128, + pub mint_denom: String, + pub start_time: Timestamp, + pub end_time: Option, + pub token_limit: Option, + pub per_address_limit: u32, + pub royalty_ratio: String, + pub payment_collector: Option, + pub whitelist_address: Option, +} + +pub type OpenEditionMinterCreateMsg = MinterInstantiateMsg; + #[cw_serde] pub enum ExecuteMsg { CreateMinter { - msg: OpenEditionMinterInstantiateMsg, + msg: OpenEditionMinterCreateMsg, }, UpdateAdmin { admin: String, diff --git a/contracts/minters/minter/src/bin/schema.rs b/contracts/minters/minter/src/bin/schema.rs index 0e80be4..904d587 100644 --- a/contracts/minters/minter/src/bin/schema.rs +++ b/contracts/minters/minter/src/bin/schema.rs @@ -2,11 +2,13 @@ use cosmwasm_schema::write_api; use omniflix_minter::msg::ExecuteMsg; -use minter_types::{InstantiateMsg, QueryMsg}; +use minter_types::QueryMsg; + +use omniflix_minter_factory::msg::CreateMinterMsg; fn main() { write_api! { - instantiate: InstantiateMsg, + instantiate: CreateMinterMsg, execute: ExecuteMsg, query: QueryMsg, } diff --git a/contracts/minters/minter/src/contract.rs b/contracts/minters/minter/src/contract.rs index 8cffd14..23105a7 100644 --- a/contracts/minters/minter/src/contract.rs +++ b/contracts/minters/minter/src/contract.rs @@ -8,9 +8,9 @@ use cosmwasm_std::{ Response, StdResult, Uint128, WasmMsg, }; use cw_utils::{maybe_addr, must_pay, nonpayable}; -use minter_types::{CollectionDetails, InstantiateMsg}; -use omniflix_minter_factory::msg::ParamsResponse; +use minter_types::CollectionDetails; use omniflix_minter_factory::msg::QueryMsg::Params as QueryFactoryParams; +use omniflix_minter_factory::msg::{CreateMinterMsg, ParamsResponse}; use omniflix_round_whitelist::msg::ExecuteMsg::PrivateMint; use whitelist_types::{ IsActiveResponse, IsMemberResponse, MintPriceResponse, RoundWhitelistQueryMsgs, @@ -47,7 +47,7 @@ pub fn instantiate( deps: DepsMut, env: Env, info: MessageInfo, - msg: InstantiateMsg, + msg: CreateMinterMsg, ) -> Result { // Query denom creation fee set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; @@ -81,31 +81,31 @@ pub fn instantiate( }); } // Check if per address limit is 0 - if msg.per_address_limit == 0 { + if msg.init.per_address_limit == 0 { return Err(ContractError::PerAddressLimitZero {}); } // Check num_tokens - if msg.num_tokens == 0 { + if msg.init.num_tokens == 0 { return Err(ContractError::InvalidNumTokens {}); } // Check start time - if msg.start_time < env.block.time { + if msg.init.start_time < env.block.time { return Err(ContractError::InvalidStartTime {}); } // Check end time - if let Some(end_time) = msg.end_time { - if end_time < msg.start_time { + if let Some(end_time) = msg.init.end_time { + if end_time < msg.init.start_time { return Err(ContractError::InvalidEndTime {}); } } // Check royalty ratio we expect decimal number - let royalty_ratio = Decimal::from_str(&msg.royalty_ratio)?; + let royalty_ratio = Decimal::from_str(&msg.init.royalty_ratio)?; if royalty_ratio < Decimal::zero() || royalty_ratio > Decimal::one() { return Err(ContractError::InvalidRoyaltyRatio {}); } // Check if whitelist already active - if let Some(whitelist_address) = msg.whitelist_address.clone() { + if let Some(whitelist_address) = msg.init.whitelist_address.clone() { let is_active: IsActiveResponse = deps.querier.query_wasm_smart( whitelist_address.clone(), &RoundWhitelistQueryMsgs::IsActive {}, @@ -116,27 +116,28 @@ pub fn instantiate( } // Check mint price - if msg.mint_price == Uint128::new(0) { + if msg.init.mint_price == Uint128::new(0) { return Err(ContractError::InvalidMintPrice {}); } - let admin = maybe_addr(deps.api, msg.admin.clone())?.unwrap_or(info.sender.clone()); + let admin = maybe_addr(deps.api, msg.init.admin.clone())?.unwrap_or(info.sender.clone()); let payment_collector = - maybe_addr(deps.api, msg.payment_collector.clone())?.unwrap_or(info.sender.clone()); - let num_tokens = msg.num_tokens; + maybe_addr(deps.api, msg.init.payment_collector.clone())?.unwrap_or(info.sender.clone()); + let num_tokens = msg.init.num_tokens; let config = Config { - per_address_limit: msg.per_address_limit, + per_address_limit: msg.init.per_address_limit, payment_collector, - start_time: msg.start_time, + start_time: msg.init.start_time, royalty_ratio, admin, mint_price: Coin { - denom: msg.mint_denom.clone(), - amount: msg.mint_price, + denom: msg.init.mint_denom.clone(), + amount: msg.init.mint_price, }, - whitelist_address: maybe_addr(deps.api, msg.whitelist_address.clone())?, - end_time: msg.end_time, + whitelist_address: maybe_addr(deps.api, msg.init.whitelist_address.clone())?, + end_time: msg.init.end_time, + token_limit: None, }; CONFIG.save(deps.storage, &config)?; diff --git a/contracts/minters/open-edition-minter/src/bin/schema.rs b/contracts/minters/open-edition-minter/src/bin/schema.rs index b7fc3a9..7535117 100644 --- a/contracts/minters/open-edition-minter/src/bin/schema.rs +++ b/contracts/minters/open-edition-minter/src/bin/schema.rs @@ -1,12 +1,14 @@ use cosmwasm_schema::write_api; -use open_edition_minter_types::{InstantiateMsg, QueryMsg}; +use open_edition_minter_types::QueryMsg; use omniflix_open_edition_minter::msg::ExecuteMsg; +use omniflix_open_edition_minter_factory::msg::OpenEditionMinterCreateMsg; + fn main() { write_api! { - instantiate: InstantiateMsg, + instantiate: OpenEditionMinterCreateMsg, execute: ExecuteMsg, query: QueryMsg, } diff --git a/contracts/minters/open-edition-minter/src/contract.rs b/contracts/minters/open-edition-minter/src/contract.rs index 8fc2aa9..79eace4 100644 --- a/contracts/minters/open-edition-minter/src/contract.rs +++ b/contracts/minters/open-edition-minter/src/contract.rs @@ -8,16 +8,15 @@ use cosmwasm_std::{ StdResult, Uint128, WasmMsg, }; use cw_utils::{maybe_addr, must_pay, nonpayable}; -use open_edition_minter_types::{ - CollectionDetails, Config, InstantiateMsg, QueryMsg, Token, UserDetails, -}; +use minter_types::{CollectionDetails, Config, Token, UserDetails}; +use open_edition_minter_types::QueryMsg; use crate::error::ContractError; use crate::msg::ExecuteMsg; use crate::state::{last_token_id, COLLECTION, CONFIG, MINTED_COUNT, MINTED_TOKENS}; use cw2::set_contract_version; use omniflix_open_edition_minter_factory::msg::{ - ParamsResponse, QueryMsg as OpenEditionMinterFactoryQueryMsg, + OpenEditionMinterCreateMsg, ParamsResponse, QueryMsg as OpenEditionMinterFactoryQueryMsg, }; use omniflix_round_whitelist::msg::ExecuteMsg as RoundWhitelistExecuteMsg; use omniflix_std::types::omniflix::onft::v1beta1::{ @@ -48,7 +47,7 @@ pub fn instantiate( deps: DepsMut, env: Env, info: MessageInfo, - msg: InstantiateMsg, + msg: OpenEditionMinterCreateMsg, ) -> Result { // Query denom creation fee set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; @@ -84,34 +83,34 @@ pub fn instantiate( }); } // Check if per address limit is 0 - if msg.per_address_limit == 0 { + if msg.init.per_address_limit == 0 { return Err(ContractError::PerAddressLimitZero {}); } // Check if token limit is 0 - if let Some(token_limit) = msg.token_limit { + if let Some(token_limit) = msg.init.token_limit { if token_limit == 0 { return Err(ContractError::InvalidNumTokens {}); } } // Check start time - if msg.start_time < env.block.time { + if msg.init.start_time < env.block.time { return Err(ContractError::InvalidStartTime {}); } // Check end time - if let Some(end_time) = msg.end_time { - if end_time < msg.start_time { + if let Some(end_time) = msg.init.end_time { + if end_time < msg.init.start_time { return Err(ContractError::InvalidEndTime {}); } } // Check royalty ratio we expect decimal number - let royalty_ratio = Decimal::from_str(&msg.royalty_ratio)?; + let royalty_ratio = Decimal::from_str(&msg.init.royalty_ratio)?; if royalty_ratio < Decimal::zero() || royalty_ratio > Decimal::one() { return Err(ContractError::InvalidRoyaltyRatio {}); } // Check if whitelist already active - if let Some(whitelist_address) = msg.whitelist_address.clone() { + if let Some(whitelist_address) = msg.init.whitelist_address.clone() { let is_active: IsActiveResponse = deps.querier.query_wasm_smart( whitelist_address.clone(), &RoundWhitelistQueryMsgs::IsActive {}, @@ -120,24 +119,24 @@ pub fn instantiate( return Err(ContractError::WhitelistAlreadyActive {}); } } - let admin = maybe_addr(deps.api, msg.admin.clone())?.unwrap_or(info.sender.clone()); + let admin = maybe_addr(deps.api, msg.init.admin.clone())?.unwrap_or(info.sender.clone()); let payment_collector = - maybe_addr(deps.api, msg.payment_collector.clone())?.unwrap_or(info.sender.clone()); + maybe_addr(deps.api, msg.init.payment_collector.clone())?.unwrap_or(info.sender.clone()); let config = Config { - per_address_limit: msg.per_address_limit, + per_address_limit: msg.init.per_address_limit, payment_collector, - start_time: msg.start_time, + start_time: msg.init.start_time, royalty_ratio, admin, mint_price: Coin { - denom: msg.mint_denom.clone(), - amount: msg.mint_price, + denom: msg.init.mint_denom.clone(), + amount: msg.init.mint_price, }, - whitelist_address: maybe_addr(deps.api, msg.whitelist_address.clone())?, - end_time: msg.end_time, - token_limit: msg.token_limit, + whitelist_address: maybe_addr(deps.api, msg.init.whitelist_address.clone())?, + end_time: msg.init.end_time, + token_limit: msg.init.token_limit, }; CONFIG.save(deps.storage, &config)?; diff --git a/contracts/minters/open-edition-minter/src/state.rs b/contracts/minters/open-edition-minter/src/state.rs index 844d17c..43e301d 100644 --- a/contracts/minters/open-edition-minter/src/state.rs +++ b/contracts/minters/open-edition-minter/src/state.rs @@ -3,7 +3,7 @@ use std::u32; use cosmwasm_std::{Addr, Storage}; use cw_storage_plus::{Item, Map}; -use open_edition_minter_types::{CollectionDetails, Config, UserDetails}; +use minter_types::{CollectionDetails, Config, UserDetails}; pub const CONFIG: Item = Item::new("config"); pub const COLLECTION: Item = Item::new("collection"); diff --git a/integration-tests/src/test_minter_creation.rs b/integration-tests/src/test_minter_creation.rs index c679a9d..abc1f55 100644 --- a/integration-tests/src/test_minter_creation.rs +++ b/integration-tests/src/test_minter_creation.rs @@ -121,7 +121,7 @@ mod test_minter_creation { // Send 0 num tokens let mut minter_inst_msg = return_minter_instantiate_msg(); - minter_inst_msg.num_tokens = 0; + minter_inst_msg.init.num_tokens = 0; let create_minter_msg = FactoryExecuteMsg::CreateMinter { msg: minter_inst_msg, }; @@ -139,7 +139,7 @@ mod test_minter_creation { // Send royalty ratio more than 100% let mut minter_inst_msg = return_minter_instantiate_msg(); - minter_inst_msg.royalty_ratio = "1.1".to_string(); + minter_inst_msg.init.royalty_ratio = "1.1".to_string(); let create_minter_msg = FactoryExecuteMsg::CreateMinter { msg: minter_inst_msg, }; @@ -157,7 +157,7 @@ mod test_minter_creation { // Send mint price 0 let mut minter_inst_msg = return_minter_instantiate_msg(); - minter_inst_msg.mint_price = Uint128::zero(); + minter_inst_msg.init.mint_price = Uint128::zero(); let create_minter_msg = FactoryExecuteMsg::CreateMinter { msg: minter_inst_msg, }; @@ -175,7 +175,7 @@ mod test_minter_creation { // Incorrect start time let mut minter_inst_msg = return_minter_instantiate_msg(); - minter_inst_msg.start_time = Timestamp::from_nanos(1_000 - 1); + minter_inst_msg.init.start_time = Timestamp::from_nanos(1_000 - 1); let create_minter_msg = FactoryExecuteMsg::CreateMinter { msg: minter_inst_msg, }; @@ -193,7 +193,7 @@ mod test_minter_creation { // Incorrect end time let mut minter_inst_msg = return_minter_instantiate_msg(); - minter_inst_msg.end_time = Some(minter_inst_msg.start_time.minus_nanos(1)); + minter_inst_msg.init.end_time = Some(minter_inst_msg.init.start_time.minus_nanos(1)); let create_minter_msg = FactoryExecuteMsg::CreateMinter { msg: minter_inst_msg, }; diff --git a/integration-tests/src/test_minting.rs b/integration-tests/src/test_minting.rs index 908cf7d..c968da7 100644 --- a/integration-tests/src/test_minting.rs +++ b/integration-tests/src/test_minting.rs @@ -570,8 +570,8 @@ mod test_minting { let round_whitelist_address = get_minter_address_from_res(res.clone()); let mut minter_inst_msg = return_minter_instantiate_msg(); - minter_inst_msg.whitelist_address = Some(round_whitelist_address.clone()); - minter_inst_msg.per_address_limit = 2; + minter_inst_msg.init.whitelist_address = Some(round_whitelist_address.clone()); + minter_inst_msg.init.per_address_limit = 2; let create_minter_msg = FactoryExecuteMsg::CreateMinter { msg: minter_inst_msg, diff --git a/integration-tests/src/test_open_edition_minter.rs b/integration-tests/src/test_open_edition_minter.rs new file mode 100644 index 0000000..0fbdaf3 --- /dev/null +++ b/integration-tests/src/test_open_edition_minter.rs @@ -0,0 +1,62 @@ +#[cfg(test)] +mod test_open_edition_minter_creation { + + use cosmwasm_std::{ + coin, to_json_binary, Addr, BlockInfo, Decimal, QueryRequest, StdError, Timestamp, Uint128, + WasmQuery, + }; + + use cw_multi_test::Executor; + use omniflix_open_edition_minter_factory::msg::{ + ExecuteMsg as OpenEditionMinterFactoryExecuteMsg, + InstantiateMsg as OpenEditionMinterFactoryInstantiateMsg, + }; + use open_edition_minter_types::InstantiateMsg as OpenEditionMinterInstantiateMsg; + + use whitelist_types::{Round, RoundWhitelistQueryMsgs}; + + use crate::utils::{get_minter_address_from_res, return_minter_instantiate_msg, return_rounds}; + + use crate::{setup::setup, utils::query_onft_collection}; + + use omniflix_open_edition_minter::msg::ExecuteMsg as OpenEditionMinterExecuteMsg; + use omniflix_round_whitelist::error::ContractError as RoundWhitelistContractError; + use omniflix_round_whitelist_factory::error::ContractError as RoundWhitelistFactoryContractError; + + #[test] + fn test_open_edition_minter_creation() { + let ( + mut app, + test_addresses, + _minter_factory_code_id, + _minter_code_id, + _round_whitelist_factory_code_id, + _round_whitelist_code_id, + open_edition_minter_factory_code_id, + open_edition_minter_code_id, + ) = setup(); + let admin = test_addresses.admin; + let creator = test_addresses.creator; + let collector = test_addresses.collector; + + // Instantiate the minter factory + let open_edition_minter_factory_instantiate_msg = OpenEditionMinterFactoryInstantiateMsg { + admin: Some(admin.to_string()), + allowed_minter_mint_denoms: vec!["uflix".to_string()], + open_edition_minter_code_id, + fee_collector_address: collector.to_string(), + minter_creation_fee: coin(1000000, "uflix"), + }; + + let minter_factory_address = app + .instantiate_contract( + open_edition_minter_factory_code_id, + admin.clone(), + &open_edition_minter_factory_instantiate_msg, + &[], + "Open Edition Minter Factory", + None, + ) + .unwrap(); + } +} diff --git a/integration-tests/src/utils.rs b/integration-tests/src/utils.rs index bd79d59..5dac021 100644 --- a/integration-tests/src/utils.rs +++ b/integration-tests/src/utils.rs @@ -1,6 +1,7 @@ use cosmwasm_std::{from_json, Addr, Coin, MemoryStorage, Storage, Timestamp, Uint128}; use cw_multi_test::AppResponse; -use minter_types::{CollectionDetails, InstantiateMsg as MinterInstantiateMsg}; +use minter_types::CollectionDetails; +use omniflix_minter_factory::msg::{CreateMinterMsg, MinterInitExtention}; use omniflix_std::types::omniflix::onft::v1beta1::Collection; pub fn get_minter_address_from_res(res: AppResponse) -> String { @@ -23,7 +24,7 @@ pub fn query_onft_collection(storage: &MemoryStorage, minter_address: String) -> collection_details } -pub fn return_minter_instantiate_msg() -> MinterInstantiateMsg { +pub fn return_minter_instantiate_msg() -> CreateMinterMsg { let collection_details = CollectionDetails { name: "name".to_string(), description: "description".to_string(), @@ -38,20 +39,22 @@ pub fn return_minter_instantiate_msg() -> MinterInstantiateMsg { uri_hash: "uri_hash".to_string(), data: "data".to_string(), }; - - MinterInstantiateMsg { - per_address_limit: 1, - admin: Some("creator".to_string()), - collection_details, - mint_denom: "uflix".to_string(), - mint_price: Uint128::from(1000000u128), - start_time: Timestamp::from_nanos(1_000_000_000), - royalty_ratio: "0.1".to_string(), - payment_collector: Some("payment_collector".to_string()), - whitelist_address: None, - end_time: Some(Timestamp::from_nanos(1_000_000_000 + 1_000_000_000)), - num_tokens: 1000, - } + let init = CreateMinterMsg { + collection_details: collection_details, + init: MinterInitExtention { + admin: Some("creator".to_string()), + mint_denom: "uflix".to_string(), + mint_price: Uint128::from(1000000u128), + start_time: Timestamp::from_nanos(1_000_000_000), + end_time: Some(Timestamp::from_nanos(2_000_000_000)), + per_address_limit: 1, + royalty_ratio: "0.1".to_string(), + payment_collector: Some("payment_collector".to_string()), + whitelist_address: None, + num_tokens: 1000, + }, + }; + init } pub fn return_rounds() -> Vec { diff --git a/packages/minter-types/src/lib.rs b/packages/minter-types/src/lib.rs index dcda7b1..bfdb8d5 100644 --- a/packages/minter-types/src/lib.rs +++ b/packages/minter-types/src/lib.rs @@ -1,26 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Coin, Decimal, Timestamp, Uint128}; -#[cw_serde] -pub struct InstantiateMsg { - pub collection_details: CollectionDetails, - pub admin: Option, - pub mint_price: Uint128, - // Factory sould check denom against the params if denoms is valid and whitelisted. - pub mint_denom: String, - // Public minting start time - pub start_time: Timestamp, - pub end_time: Option, - pub per_address_limit: u32, - // We expect user to send a string between 0 and 1 - // FE "0.1" - pub royalty_ratio: String, - pub payment_collector: Option, - // Whitelist address if any - pub whitelist_address: Option, - pub num_tokens: u32, -} - #[cw_serde] pub struct CollectionDetails { pub name: String, @@ -37,6 +17,12 @@ pub struct CollectionDetails { pub data: String, } +#[cw_serde] +pub struct MinterInstantiateMsg { + pub collection_details: CollectionDetails, + pub init: T, +} + #[cw_serde] pub struct UserDetails { pub minted_tokens: Vec, @@ -82,4 +68,5 @@ pub struct Config { pub royalty_ratio: Decimal, pub admin: Addr, pub whitelist_address: Option, + pub token_limit: Option, } diff --git a/packages/open-edition-minter-types/Cargo.toml b/packages/open-edition-minter-types/Cargo.toml index 2d4f74c..41a5b35 100644 --- a/packages/open-edition-minter-types/Cargo.toml +++ b/packages/open-edition-minter-types/Cargo.toml @@ -31,4 +31,5 @@ cosmwasm-std = { workspace = true } cw721-base = { workspace = true, features = ["library"] } cw-utils = { workspace = true } thiserror = { workspace = true } -serde = { workspace = true } \ No newline at end of file +serde = { workspace = true } +minter-types = { workspace = true } \ No newline at end of file diff --git a/packages/open-edition-minter-types/src/lib.rs b/packages/open-edition-minter-types/src/lib.rs index 241907f..a8cf4cc 100644 --- a/packages/open-edition-minter-types/src/lib.rs +++ b/packages/open-edition-minter-types/src/lib.rs @@ -1,56 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Coin, Decimal, Timestamp, Uint128}; - -#[cw_serde] -pub struct InstantiateMsg { - pub collection_details: CollectionDetails, - pub admin: Option, - pub mint_price: Uint128, - pub mint_denom: String, - pub start_time: Timestamp, - pub end_time: Option, - pub token_limit: Option, - pub per_address_limit: u32, - pub royalty_ratio: String, - pub payment_collector: Option, - pub whitelist_address: Option, -} - -#[cw_serde] -pub struct CollectionDetails { - pub name: String, - pub description: String, - pub preview_uri: String, - pub schema: String, - pub symbol: String, - pub id: String, - pub extensible: bool, - pub nsfw: bool, - pub base_uri: String, - pub uri: String, - pub uri_hash: String, - pub data: String, -} - -#[cw_serde] -pub struct UserDetails { - pub minted_tokens: Vec, - pub total_minted_count: u32, -} - -impl Default for UserDetails { - fn default() -> Self { - UserDetails { - minted_tokens: Vec::new(), - total_minted_count: 0, - } - } -} - -#[cw_serde] -pub struct Token { - pub token_id: String, -} +use minter_types::{CollectionDetails, Config, UserDetails}; #[cw_serde] #[derive(QueryResponses)] @@ -66,16 +15,3 @@ pub enum QueryMsg { #[returns(u32)] TokensRemaining {}, } - -#[cw_serde] -pub struct Config { - pub per_address_limit: u32, - pub payment_collector: Addr, - pub start_time: Timestamp, - pub end_time: Option, - pub mint_price: Coin, - pub royalty_ratio: Decimal, - pub admin: Addr, - pub whitelist_address: Option, - pub token_limit: Option, -} From 07ab88d609a68f52ec109c839b456bee99125613 Mon Sep 17 00:00:00 2001 From: Ninjatosba Date: Mon, 22 Jan 2024 15:16:12 +0300 Subject: [PATCH 08/11] linter --- .../factories/open-edition-minter-factory/src/contract.rs | 2 +- contracts/factories/open-edition-minter-factory/src/msg.rs | 2 +- contracts/minters/minter/src/state.rs | 5 +++-- packages/minter-types/src/lib.rs | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/contracts/factories/open-edition-minter-factory/src/contract.rs b/contracts/factories/open-edition-minter-factory/src/contract.rs index a13b9ab..e69a3cb 100644 --- a/contracts/factories/open-edition-minter-factory/src/contract.rs +++ b/contracts/factories/open-edition-minter-factory/src/contract.rs @@ -11,7 +11,6 @@ use cosmwasm_std::{ StdResult, Uint128, WasmMsg, }; use cw_utils::maybe_addr; -use minter_types::CollectionDetails; use omniflix_std::types::omniflix::onft::v1beta1::OnftQuerier; use std::str::FromStr; #[cfg(not(test))] @@ -233,6 +232,7 @@ mod tests { testing::{mock_dependencies, mock_env, mock_info}, Addr, Decimal, Timestamp, }; + use minter_types::CollectionDetails; #[test] fn test_instantiate() { diff --git a/contracts/factories/open-edition-minter-factory/src/msg.rs b/contracts/factories/open-edition-minter-factory/src/msg.rs index 512fb3c..5790f9e 100644 --- a/contracts/factories/open-edition-minter-factory/src/msg.rs +++ b/contracts/factories/open-edition-minter-factory/src/msg.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Coin, Timestamp, Uint128}; -use minter_types::{CollectionDetails, MinterInstantiateMsg}; +use minter_types::MinterInstantiateMsg; use crate::state::Params; #[cw_serde] diff --git a/contracts/minters/minter/src/state.rs b/contracts/minters/minter/src/state.rs index e18c08c..b01d75b 100644 --- a/contracts/minters/minter/src/state.rs +++ b/contracts/minters/minter/src/state.rs @@ -1,7 +1,6 @@ use std::u32; - -use cosmwasm_std::{Addr}; +use cosmwasm_std::Addr; use cw_storage_plus::{Item, Map}; use minter_types::{CollectionDetails, Config, Token, UserDetails}; @@ -14,3 +13,5 @@ pub const MINTABLE_TOKENS: Map = Map::new("mintable_tokens"); pub const TOTAL_TOKENS_REMAINING: Item = Item::new("total_tokens_remaining"); // Address and number of tokens minted pub const MINTED_TOKENS: Map = Map::new("minted_tokens"); + +// Add Frozen state diff --git a/packages/minter-types/src/lib.rs b/packages/minter-types/src/lib.rs index bfdb8d5..7792c35 100644 --- a/packages/minter-types/src/lib.rs +++ b/packages/minter-types/src/lib.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Coin, Decimal, Timestamp, Uint128}; +use cosmwasm_std::{Addr, Coin, Decimal, Timestamp}; #[cw_serde] pub struct CollectionDetails { From 2c4cbe6168973f4564ce201dc7a4d6310256e477 Mon Sep 17 00:00:00 2001 From: Ninjatosba Date: Mon, 22 Jan 2024 17:17:17 +0300 Subject: [PATCH 09/11] test open edition minter creation --- Cargo.lock | 3 + .../open-edition-minter/src/contract.rs | 6 +- integration-tests/Cargo.toml | 3 + integration-tests/src/lib.rs | 1 + integration-tests/src/setup.rs | 40 ++- integration-tests/src/test_minter_creation.rs | 4 + integration-tests/src/test_minting.rs | 6 + .../src/test_open_edition_minter.rs | 312 +++++++++++++++++- .../src/test_round_whitelist_creation.rs | 2 + integration-tests/src/utils.rs | 39 ++- 10 files changed, 396 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3bc291d..7c3bbfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -644,10 +644,13 @@ dependencies = [ "minter-types", "omniflix-minter", "omniflix-minter-factory", + "omniflix-open-edition-minter", + "omniflix-open-edition-minter-factory", "omniflix-round-whitelist", "omniflix-round-whitelist-factory", "omniflix-std", "omniflix-testing", + "open-edition-minter-types", "prost 0.12.3", "schemars", "serde", diff --git a/contracts/minters/open-edition-minter/src/contract.rs b/contracts/minters/open-edition-minter/src/contract.rs index 79eace4..210929b 100644 --- a/contracts/minters/open-edition-minter/src/contract.rs +++ b/contracts/minters/open-edition-minter/src/contract.rs @@ -52,7 +52,7 @@ pub fn instantiate( // Query denom creation fee set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; // Query factory params of instantiator - // If the instantiator is not a our factory then we wont be able to parse the response + // If the instantiator is not our factory then we wont be able to parse the response let _factory_params: ParamsResponse = deps.querier.query_wasm_smart( info.sender.clone().into_string(), &OpenEditionMinterFactoryQueryMsg::Params {}, @@ -557,14 +557,14 @@ fn query_minted_tokens( } fn query_total_tokens_minted(deps: Deps, _env: Env) -> Result { - let total_tokens = MINTED_COUNT.load(deps.storage)?; + let total_tokens = MINTED_COUNT.load(deps.storage).unwrap_or(0); Ok(total_tokens) } fn query_tokens_remaining(deps: Deps, _env: Env) -> Result { let config = CONFIG.load(deps.storage)?; if let Some(token_limit) = config.token_limit { - let total_tokens = MINTED_COUNT.load(deps.storage)?; + let total_tokens = MINTED_COUNT.load(deps.storage).unwrap_or(0); Ok(token_limit - total_tokens) } else { Err(ContractError::TokenLimitNotSet {}) diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 43ea476..5cf15cf 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -27,8 +27,11 @@ schemars = "0.8.16" cw-multi-test = "0.20.0" minter-types={ workspace = true } whitelist-types ={ workspace = true } +open-edition-minter-types = { workspace = true } omniflix-minter-factory = {path = "../contracts/factories/minter-factory"} +omniflix-open-edition-minter-factory = {path = "../contracts/factories/open-edition-minter-factory"} omniflix-minter = {path = "../contracts/minters/minter"} +omniflix-open-edition-minter = {path = "../contracts/minters/open-edition-minter"} omniflix-round-whitelist-factory = {path = "../contracts/factories/round-whitelist-factory"} omniflix-round-whitelist = {path = "../contracts/whitelists/round-whitelist"} omniflix-testing = {path = "../packages/testing"} diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index f0a85d8..2a42878 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -1,5 +1,6 @@ pub mod setup; pub mod test_minter_creation; pub mod test_minting; +pub mod test_open_edition_minter; pub mod test_round_whitelist_creation; pub mod utils; diff --git a/integration-tests/src/setup.rs b/integration-tests/src/setup.rs index d3f23de..fb1becc 100644 --- a/integration-tests/src/setup.rs +++ b/integration-tests/src/setup.rs @@ -1,7 +1,4 @@ -use cosmwasm_std::{ - coins, Addr, BlockInfo, Coin, - Timestamp, -}; +use cosmwasm_std::{coins, Addr, BlockInfo, Coin, Timestamp}; use cw_multi_test::{BankSudo, ContractWrapper, SudoMsg}; use omniflix_minter::contract::{ execute as minter_execute, instantiate as minter_instantiate, query as minter_query, @@ -9,6 +6,20 @@ use omniflix_minter::contract::{ use omniflix_minter_factory::contract::{ execute as factory_execute, instantiate as factory_instantiate, query as factory_query, }; +use omniflix_open_edition_minter::contract::{ + execute as open_edition_minter_execute, instantiate as open_edition_minter_instantiate, + query as open_edition_minter_query, +}; + +use omniflix_open_edition_minter_factory::contract::{ + execute as open_edition_minter_factory_execute, + instantiate as open_edition_minter_factory_instantiate, + query as open_edition_minter_factory_query, +}; +use omniflix_open_edition_minter_factory::msg::{ + ExecuteMsg as OpenEditionMinterFactoryExecuteMsg, + InstantiateMsg as OpenEditionMinterFactoryInstantiateMsg, +}; use omniflix_round_whitelist::contract::{ execute as round_whitelist_execute, instantiate as round_whitelist_instantiate, query as round_whitelist_query, @@ -18,16 +29,13 @@ use omniflix_round_whitelist_factory::contract::{ query as round_whitelist_factory_query, }; - - - use omniflix_testing::app::OmniflixApp; pub struct TestAdresses { pub admin: Addr, pub creator: Addr, pub collector: Addr, } -pub fn setup() -> (OmniflixApp, TestAdresses, u64, u64, u64, u64) { +pub fn setup() -> (OmniflixApp, TestAdresses, u64, u64, u64, u64, u64, u64) { let mut app = OmniflixApp::new(); let admin = Addr::unchecked("admin"); let creator = Addr::unchecked("creator"); @@ -83,6 +91,16 @@ pub fn setup() -> (OmniflixApp, TestAdresses, u64, u64, u64, u64) { round_whitelist_instantiate, round_whitelist_query, )); + let open_edition_minter_factory_contract = Box::new(ContractWrapper::new( + open_edition_minter_factory_execute, + open_edition_minter_factory_instantiate, + open_edition_minter_factory_query, + )); + let open_edition_minter_contract = Box::new(ContractWrapper::new( + open_edition_minter_execute, + open_edition_minter_instantiate, + open_edition_minter_query, + )); let minter_code_id = app.store_code(minter_contract); @@ -92,6 +110,10 @@ pub fn setup() -> (OmniflixApp, TestAdresses, u64, u64, u64, u64) { let round_whitelist_factory_code_id = app.store_code(round_whitelist_factory_contract); + let open_edition_minter_code_id = app.store_code(open_edition_minter_contract); + + let open_edition_minter_factory_code_id = app.store_code(open_edition_minter_factory_contract); + ( app, TestAdresses { @@ -103,6 +125,8 @@ pub fn setup() -> (OmniflixApp, TestAdresses, u64, u64, u64, u64) { minter_code_id, round_whitelist_factory_code_id, round_whitelist_code_id, + open_edition_minter_factory_code_id, + open_edition_minter_code_id, ) } fn mint_to_address(app: &mut OmniflixApp, to_address: String, amount: Vec) { diff --git a/integration-tests/src/test_minter_creation.rs b/integration-tests/src/test_minter_creation.rs index abc1f55..8bea18b 100644 --- a/integration-tests/src/test_minter_creation.rs +++ b/integration-tests/src/test_minter_creation.rs @@ -34,6 +34,8 @@ mod test_minter_creation { minter_code_id, _round_whitelist_factory_code_id, _round_whitelist_code_id, + _open_edition_minter_factory_code_id, + _open_edition_minter_code_id, ) = setup(); let admin = test_addresses.admin; let creator = test_addresses.creator; @@ -297,6 +299,8 @@ mod test_minter_creation { _minter_code_id, round_whitelist_factory_code_id, round_whitelist_code_id, + _open_edition_minter_factory_code_id, + _open_edition_minter_code_id, ) = setup(); let admin = test_addresses.admin; let creator = test_addresses.creator; diff --git a/integration-tests/src/test_minting.rs b/integration-tests/src/test_minting.rs index c968da7..da3e754 100644 --- a/integration-tests/src/test_minting.rs +++ b/integration-tests/src/test_minting.rs @@ -30,6 +30,8 @@ mod test_minting { minter_code_id, _round_whitelist_factory_code_id, _round_whitelist_code_id, + _open_edition_minter_code_id, + _open_edition_minter_factory_code_id, ) = setup(); let admin = test_addresses.admin; let creator = test_addresses.creator; @@ -304,6 +306,8 @@ mod test_minting { minter_code_id, _round_whitelist_factory_code_id, _round_whitelist_code_id, + _open_edition_minter_code_id, + _open_edition_minter_factory_code_id, ) = setup(); let admin = test_addresses.admin; let creator = test_addresses.creator; @@ -499,6 +503,8 @@ mod test_minting { minter_code_id, round_whitelist_factory_code_id, round_whitelist_code_id, + _open_edition_minter_code_id, + _open_edition_minter_factory_code_id, ) = setup(); let admin = test_addresses.admin; let creator = test_addresses.creator; diff --git a/integration-tests/src/test_open_edition_minter.rs b/integration-tests/src/test_open_edition_minter.rs index 0fbdaf3..6975f93 100644 --- a/integration-tests/src/test_open_edition_minter.rs +++ b/integration-tests/src/test_open_edition_minter.rs @@ -2,26 +2,33 @@ mod test_open_edition_minter_creation { use cosmwasm_std::{ - coin, to_json_binary, Addr, BlockInfo, Decimal, QueryRequest, StdError, Timestamp, Uint128, - WasmQuery, + coin, from_json, to_json_binary, Addr, BlockInfo, Coin, Decimal, QueryRequest, StdError, + Timestamp, Uint128, WasmQuery, }; use cw_multi_test::Executor; + use minter_types::{CollectionDetails, Config}; use omniflix_open_edition_minter_factory::msg::{ ExecuteMsg as OpenEditionMinterFactoryExecuteMsg, InstantiateMsg as OpenEditionMinterFactoryInstantiateMsg, }; - use open_edition_minter_types::InstantiateMsg as OpenEditionMinterInstantiateMsg; use whitelist_types::{Round, RoundWhitelistQueryMsgs}; - use crate::utils::{get_minter_address_from_res, return_minter_instantiate_msg, return_rounds}; + use crate::utils::{ + get_minter_address_from_res, return_minter_instantiate_msg, + return_open_edition_minter_inst_msg, return_rounds, + }; use crate::{setup::setup, utils::query_onft_collection}; use omniflix_open_edition_minter::msg::ExecuteMsg as OpenEditionMinterExecuteMsg; - use omniflix_round_whitelist::error::ContractError as RoundWhitelistContractError; - use omniflix_round_whitelist_factory::error::ContractError as RoundWhitelistFactoryContractError; + + use open_edition_minter_types::QueryMsg as OpenEditionMinterQueryMsg; + + use omniflix_open_edition_minter::error::ContractError as OpenEditionMinterError; + + use omniflix_open_edition_minter_factory::error::ContractError as OpenEditionMinterFactoryError; #[test] fn test_open_edition_minter_creation() { @@ -44,11 +51,11 @@ mod test_open_edition_minter_creation { admin: Some(admin.to_string()), allowed_minter_mint_denoms: vec!["uflix".to_string()], open_edition_minter_code_id, - fee_collector_address: collector.to_string(), + fee_collector_address: admin.to_string(), minter_creation_fee: coin(1000000, "uflix"), }; - let minter_factory_address = app + let open_edition_minter_factory_address = app .instantiate_contract( open_edition_minter_factory_code_id, admin.clone(), @@ -58,5 +65,294 @@ mod test_open_edition_minter_creation { None, ) .unwrap(); + + // Create a minter + let open_edition_minter_instantiate_msg = return_open_edition_minter_inst_msg(); + let create_minter_msg = OpenEditionMinterFactoryExecuteMsg::CreateMinter { + msg: open_edition_minter_instantiate_msg, + }; + // Send no funds + let res = app + .execute_contract( + creator.clone(), + open_edition_minter_factory_address.clone(), + &create_minter_msg, + &[], + ) + .unwrap_err(); + let err = res.source().unwrap(); + let error = err.downcast_ref::().unwrap(); + assert_eq!( + OpenEditionMinterFactoryError::IncorrectFunds { + expected: [ + Coin { + denom: "uflix".to_string(), + amount: Uint128::from(1000000u128) + }, + Coin { + denom: "uflix".to_string(), + amount: Uint128::from(1000000u128) + } + ] + .to_vec(), + actual: vec![] + }, + *error + ); + + // Send incorrect funds + let res = app + .execute_contract( + creator.clone(), + open_edition_minter_factory_address.clone(), + &create_minter_msg, + &[coin(1000000, "incorrect_denom")], + ) + .unwrap_err(); + let err = res.source().unwrap(); + let error = err.downcast_ref::().unwrap(); + assert_eq!( + OpenEditionMinterFactoryError::IncorrectFunds { + expected: [ + Coin { + denom: "uflix".to_string(), + amount: Uint128::from(1000000u128) + }, + Coin { + denom: "uflix".to_string(), + amount: Uint128::from(1000000u128) + } + ] + .to_vec(), + actual: vec![coin(1000000, "incorrect_denom")] + }, + *error + ); + + // Send incorrect amount + let res = app + .execute_contract( + creator.clone(), + open_edition_minter_factory_address.clone(), + &create_minter_msg, + &[coin(1000000, "uflix")], + ) + .unwrap_err(); + let err = res.source().unwrap(); + let error = err.downcast_ref::().unwrap(); + assert_eq!( + OpenEditionMinterFactoryError::IncorrectFunds { + expected: [ + Coin { + denom: "uflix".to_string(), + amount: Uint128::from(1000000u128) + }, + Coin { + denom: "uflix".to_string(), + amount: Uint128::from(1000000u128) + } + ] + .to_vec(), + actual: vec![coin(1000000, "uflix")] + }, + *error + ); + + // Send zero token limit + let mut open_edition_minter_instantiate_msg = return_open_edition_minter_inst_msg(); + open_edition_minter_instantiate_msg.init.token_limit = Some(0); + let create_minter_msg = OpenEditionMinterFactoryExecuteMsg::CreateMinter { + msg: open_edition_minter_instantiate_msg, + }; + let res = app + .execute_contract( + creator.clone(), + open_edition_minter_factory_address.clone(), + &create_minter_msg, + &[coin(2000000, "uflix")], + ) + .unwrap_err(); + + let err = res.source().unwrap().source().unwrap(); + + let error = err.downcast_ref::().unwrap(); + assert_eq!(OpenEditionMinterError::InvalidNumTokens {}, *error); + + // Send zero per address limit + let mut open_edition_minter_instantiate_msg = return_open_edition_minter_inst_msg(); + open_edition_minter_instantiate_msg.init.per_address_limit = 0; + let create_minter_msg = OpenEditionMinterFactoryExecuteMsg::CreateMinter { + msg: open_edition_minter_instantiate_msg, + }; + let res = app + .execute_contract( + creator.clone(), + open_edition_minter_factory_address.clone(), + &create_minter_msg, + &[coin(2000000, "uflix")], + ) + .unwrap_err(); + + let err = res.source().unwrap().source().unwrap(); + + let error = err.downcast_ref::().unwrap(); + assert_eq!(OpenEditionMinterError::PerAddressLimitZero {}, *error); + + // Send incorrect royalty ratio + let mut open_edition_minter_instantiate_msg = return_open_edition_minter_inst_msg(); + open_edition_minter_instantiate_msg.init.royalty_ratio = "1.1".to_string(); + let create_minter_msg = OpenEditionMinterFactoryExecuteMsg::CreateMinter { + msg: open_edition_minter_instantiate_msg, + }; + let res = app + .execute_contract( + creator.clone(), + open_edition_minter_factory_address.clone(), + &create_minter_msg, + &[coin(2000000, "uflix")], + ) + .unwrap_err(); + + let err = res.source().unwrap().source().unwrap(); + + let error = err.downcast_ref::().unwrap(); + assert_eq!(OpenEditionMinterError::InvalidRoyaltyRatio {}, *error); + + // Send incorrect mint price this should not fail because mint price can be set to zero on open edition minter + let mut open_edition_minter_instantiate_msg = return_open_edition_minter_inst_msg(); + open_edition_minter_instantiate_msg.init.mint_price = Uint128::zero(); + let create_minter_msg = OpenEditionMinterFactoryExecuteMsg::CreateMinter { + msg: open_edition_minter_instantiate_msg, + }; + let res = app + .execute_contract( + creator.clone(), + open_edition_minter_factory_address.clone(), + &create_minter_msg, + &[coin(2000000, "uflix")], + ) + .unwrap(); + + // Send incorrect start time + let mut open_edition_minter_instantiate_msg = return_open_edition_minter_inst_msg(); + open_edition_minter_instantiate_msg.init.start_time = Timestamp::from_nanos(0); + let create_minter_msg = OpenEditionMinterFactoryExecuteMsg::CreateMinter { + msg: open_edition_minter_instantiate_msg, + }; + let res = app + .execute_contract( + creator.clone(), + open_edition_minter_factory_address.clone(), + &create_minter_msg, + &[coin(2000000, "uflix")], + ) + .unwrap_err(); + + let err = res.source().unwrap().source().unwrap(); + + let error = err.downcast_ref::().unwrap(); + assert_eq!(OpenEditionMinterError::InvalidStartTime {}, *error); + + // Check factory admin balance before happy path + let query_res = app + .wrap() + .query_balance(admin.clone(), "uflix".to_string()) + .unwrap(); + let uflix_before = query_res.amount; + + // Create a minter + let open_edition_minter_instantiate_msg = return_open_edition_minter_inst_msg(); + let create_minter_msg = OpenEditionMinterFactoryExecuteMsg::CreateMinter { + msg: open_edition_minter_instantiate_msg, + }; + let res = app + .execute_contract( + creator.clone(), + open_edition_minter_factory_address.clone(), + &create_minter_msg, + &[coin(2000000, "uflix")], + ) + .unwrap(); + let open_edition_minter_address = get_minter_address_from_res(res); + + // Check factory admin balance after happy path + let query_res = app + .wrap() + .query_balance(admin.clone(), "uflix".to_string()) + .unwrap(); + let uflix_after = query_res.amount; + // We are collecting fee as expected + assert_eq!(uflix_after - uflix_before, Uint128::from(1000000u128)); + + // Query the minter + let query_msg = OpenEditionMinterQueryMsg::Config {}; + + let config_res: Config = app + .wrap() + .query_wasm_smart(open_edition_minter_address.clone(), &query_msg) + .unwrap(); + assert_eq!( + config_res, + Config { + admin: Addr::unchecked(creator.clone()), + payment_collector: Addr::unchecked(creator.clone()), + end_time: Some(Timestamp::from_nanos(2_000_000_000)), + start_time: Timestamp::from_nanos(1_000_000_000), + mint_price: Coin { + denom: "uflix".to_string(), + amount: Uint128::from(1000000u128) + }, + per_address_limit: 1, + royalty_ratio: Decimal::percent(10), + whitelist_address: None, + token_limit: Some(1000), + } + ); + + // Query the minter + let query_msg = OpenEditionMinterQueryMsg::TokensRemaining {}; + + let tokens_remaining_res: u32 = app + .wrap() + .query_wasm_smart(open_edition_minter_address.clone(), &query_msg) + .unwrap(); + + assert_eq!(tokens_remaining_res, 1000); + + // Query the minter + let query_msg = OpenEditionMinterQueryMsg::TotalMintedCount {}; + + let total_minted_count_res: u32 = app + .wrap() + .query_wasm_smart(open_edition_minter_address.clone(), &query_msg) + .unwrap(); + + assert_eq!(total_minted_count_res, 0); + + // Query the minter + let query_msg = OpenEditionMinterQueryMsg::Collection {}; + + let collection_res: CollectionDetails = app + .wrap() + .query_wasm_smart(open_edition_minter_address.clone(), &query_msg) + .unwrap(); + + assert_eq!( + collection_res, + CollectionDetails { + name: "name".to_string(), + description: "description".to_string(), + preview_uri: "preview_uri".to_string(), + schema: "schema".to_string(), + symbol: "symbol".to_string(), + id: "id".to_string(), + extensible: true, + nsfw: false, + base_uri: "base_uri".to_string(), + uri: "uri".to_string(), + uri_hash: "uri_hash".to_string(), + data: "data".to_string(), + } + ); } } diff --git a/integration-tests/src/test_round_whitelist_creation.rs b/integration-tests/src/test_round_whitelist_creation.rs index c55720f..b45bb05 100644 --- a/integration-tests/src/test_round_whitelist_creation.rs +++ b/integration-tests/src/test_round_whitelist_creation.rs @@ -25,6 +25,8 @@ mod test_round_whitelist_creation { _minter_code_id, round_whitelist_factory_code_id, round_whitelist_code_id, + _open_edition_minter_code_id, + _open_edition_minter_factory_code_id, ) = setup(); let admin = test_addresses.admin; let creator = test_addresses.creator; diff --git a/integration-tests/src/utils.rs b/integration-tests/src/utils.rs index 5dac021..74d0d0d 100644 --- a/integration-tests/src/utils.rs +++ b/integration-tests/src/utils.rs @@ -2,6 +2,9 @@ use cosmwasm_std::{from_json, Addr, Coin, MemoryStorage, Storage, Timestamp, Uin use cw_multi_test::AppResponse; use minter_types::CollectionDetails; use omniflix_minter_factory::msg::{CreateMinterMsg, MinterInitExtention}; +use omniflix_open_edition_minter_factory::msg::{ + OpenEditionMinterCreateMsg, OpenEditionMinterInitExtention, +}; use omniflix_std::types::omniflix::onft::v1beta1::Collection; pub fn get_minter_address_from_res(res: AppResponse) -> String { @@ -49,7 +52,7 @@ pub fn return_minter_instantiate_msg() -> CreateMinterMsg { end_time: Some(Timestamp::from_nanos(2_000_000_000)), per_address_limit: 1, royalty_ratio: "0.1".to_string(), - payment_collector: Some("payment_collector".to_string()), + payment_collector: Some("creator".to_string()), whitelist_address: None, num_tokens: 1000, }, @@ -57,6 +60,40 @@ pub fn return_minter_instantiate_msg() -> CreateMinterMsg { init } +pub fn return_open_edition_minter_inst_msg() -> OpenEditionMinterCreateMsg { + let collection_details = CollectionDetails { + name: "name".to_string(), + description: "description".to_string(), + preview_uri: "preview_uri".to_string(), + schema: "schema".to_string(), + symbol: "symbol".to_string(), + id: "id".to_string(), + extensible: true, + nsfw: false, + base_uri: "base_uri".to_string(), + uri: "uri".to_string(), + uri_hash: "uri_hash".to_string(), + data: "data".to_string(), + }; + let init = OpenEditionMinterInitExtention { + admin: Some("creator".to_string()), + mint_denom: "uflix".to_string(), + mint_price: Uint128::from(1000000u128), + start_time: Timestamp::from_nanos(1_000_000_000), + end_time: Some(Timestamp::from_nanos(2_000_000_000)), + per_address_limit: 1, + royalty_ratio: "0.1".to_string(), + payment_collector: Some("creator".to_string()), + whitelist_address: None, + token_limit: Some(1000), + }; + let open_edition_minter_inst_msg = OpenEditionMinterCreateMsg { + collection_details: collection_details, + init: init, + }; + open_edition_minter_inst_msg +} + pub fn return_rounds() -> Vec { // Lets create 2 rounds let round_1 = whitelist_types::Round { From 104b1e0701856537821644f27504b5c87175764a Mon Sep 17 00:00:00 2001 From: Ninjatosba Date: Mon, 22 Jan 2024 17:27:08 +0300 Subject: [PATCH 10/11] add more cases --- integration-tests/src/setup.rs | 4 ---- integration-tests/src/test_minter_creation.rs | 5 +---- integration-tests/src/test_minting.rs | 17 +++++++++++++++++ .../src/test_open_edition_minter.rs | 11 +++++++++-- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/integration-tests/src/setup.rs b/integration-tests/src/setup.rs index fb1becc..a3e9334 100644 --- a/integration-tests/src/setup.rs +++ b/integration-tests/src/setup.rs @@ -16,10 +16,6 @@ use omniflix_open_edition_minter_factory::contract::{ instantiate as open_edition_minter_factory_instantiate, query as open_edition_minter_factory_query, }; -use omniflix_open_edition_minter_factory::msg::{ - ExecuteMsg as OpenEditionMinterFactoryExecuteMsg, - InstantiateMsg as OpenEditionMinterFactoryInstantiateMsg, -}; use omniflix_round_whitelist::contract::{ execute as round_whitelist_execute, instantiate as round_whitelist_instantiate, query as round_whitelist_query, diff --git a/integration-tests/src/test_minter_creation.rs b/integration-tests/src/test_minter_creation.rs index 8bea18b..d21706d 100644 --- a/integration-tests/src/test_minter_creation.rs +++ b/integration-tests/src/test_minter_creation.rs @@ -262,10 +262,7 @@ mod test_minter_creation { Decimal::from_ratio(1u128, 10u128) ); assert_eq!(config_data.admin, Addr::unchecked("creator")); - assert_eq!( - config_data.payment_collector, - Addr::unchecked("payment_collector") - ); + assert_eq!(config_data.payment_collector, Addr::unchecked("creator")); // Query mintable tokens let mintable_tokens_data: Vec = app diff --git a/integration-tests/src/test_minting.rs b/integration-tests/src/test_minting.rs index da3e754..ef57e21 100644 --- a/integration-tests/src/test_minting.rs +++ b/integration-tests/src/test_minting.rs @@ -154,6 +154,12 @@ mod test_minting { time: Timestamp::from_nanos(1_000_000_000 + 1), }); + // Query uflix balance of creator before mint + let creator_balance_before_mint: Uint128 = app + .wrap() + .query_balance(creator.to_string(), "uflix".to_string()) + .unwrap() + .amount; // Mint let res = app .execute_contract( @@ -163,6 +169,17 @@ mod test_minting { &[coin(1000000, "uflix")], ) .unwrap(); + // Query uflix balance of creator after mint + let creator_balance_after_mint: Uint128 = app + .wrap() + .query_balance(creator.to_string(), "uflix".to_string()) + .unwrap() + .amount; + // Check if creator got paid + assert_eq!( + creator_balance_after_mint, + creator_balance_before_mint + Uint128::from(1000000u128) + ); let token_id: String = res.events[1].attributes[2].value.clone(); let collection_id: String = res.events[1].attributes[3].value.clone(); // We are quering collection to check if it is minted from our mocked onft keeper diff --git a/integration-tests/src/test_open_edition_minter.rs b/integration-tests/src/test_open_edition_minter.rs index 6975f93..4ba8762 100644 --- a/integration-tests/src/test_open_edition_minter.rs +++ b/integration-tests/src/test_open_edition_minter.rs @@ -12,8 +12,7 @@ mod test_open_edition_minter_creation { ExecuteMsg as OpenEditionMinterFactoryExecuteMsg, InstantiateMsg as OpenEditionMinterFactoryInstantiateMsg, }; - - use whitelist_types::{Round, RoundWhitelistQueryMsgs}; + use omniflix_std::types::omniflix::onft::v1beta1::Collection; use crate::utils::{ get_minter_address_from_res, return_minter_instantiate_msg, @@ -354,5 +353,13 @@ mod test_open_edition_minter_creation { data: "data".to_string(), } ); + let collection = query_onft_collection(app.storage(), open_edition_minter_address.clone()); + + assert_eq!(collection.denom.clone().unwrap().name, "name".to_string()); + assert_eq!( + collection.denom.unwrap().description, + "description".to_string() + ); } + // Query onft collection } From f5ff885b9b32e2167c7b2598078305ce5b042607 Mon Sep 17 00:00:00 2001 From: Ninjatosba Date: Mon, 22 Jan 2024 18:35:37 +0300 Subject: [PATCH 11/11] test open edition minter minting --- .../open-edition-minter/src/contract.rs | 3 +- integration-tests/src/lib.rs | 1 + .../src/test_open_edition_minter.rs | 18 +- .../src/test_open_edition_minting.rs | 288 ++++++++++++++++++ 4 files changed, 295 insertions(+), 15 deletions(-) create mode 100644 integration-tests/src/test_open_edition_minting.rs diff --git a/contracts/minters/open-edition-minter/src/contract.rs b/contracts/minters/open-edition-minter/src/contract.rs index 210929b..d283778 100644 --- a/contracts/minters/open-edition-minter/src/contract.rs +++ b/contracts/minters/open-edition-minter/src/contract.rs @@ -139,6 +139,7 @@ pub fn instantiate( token_limit: msg.init.token_limit, }; CONFIG.save(deps.storage, &config)?; + MINTED_COUNT.save(deps.storage, &0)?; let collection = CollectionDetails { name: msg.collection_details.name, @@ -239,7 +240,7 @@ pub fn execute_mint(deps: DepsMut, env: Env, info: MessageInfo) -> Result config.start_time; + let is_public = env.block.time >= config.start_time; let mut messages: Vec = vec![]; diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index 2a42878..8014b73 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -2,5 +2,6 @@ pub mod setup; pub mod test_minter_creation; pub mod test_minting; pub mod test_open_edition_minter; +pub mod test_open_edition_minting; pub mod test_round_whitelist_creation; pub mod utils; diff --git a/integration-tests/src/test_open_edition_minter.rs b/integration-tests/src/test_open_edition_minter.rs index 4ba8762..cb08a5e 100644 --- a/integration-tests/src/test_open_edition_minter.rs +++ b/integration-tests/src/test_open_edition_minter.rs @@ -1,10 +1,7 @@ #[cfg(test)] mod test_open_edition_minter_creation { - use cosmwasm_std::{ - coin, from_json, to_json_binary, Addr, BlockInfo, Coin, Decimal, QueryRequest, StdError, - Timestamp, Uint128, WasmQuery, - }; + use cosmwasm_std::{coin, Addr, Coin, Decimal, Timestamp, Uint128}; use cw_multi_test::Executor; use minter_types::{CollectionDetails, Config}; @@ -12,17 +9,11 @@ mod test_open_edition_minter_creation { ExecuteMsg as OpenEditionMinterFactoryExecuteMsg, InstantiateMsg as OpenEditionMinterFactoryInstantiateMsg, }; - use omniflix_std::types::omniflix::onft::v1beta1::Collection; - use crate::utils::{ - get_minter_address_from_res, return_minter_instantiate_msg, - return_open_edition_minter_inst_msg, return_rounds, - }; + use crate::utils::{get_minter_address_from_res, return_open_edition_minter_inst_msg}; use crate::{setup::setup, utils::query_onft_collection}; - use omniflix_open_edition_minter::msg::ExecuteMsg as OpenEditionMinterExecuteMsg; - use open_edition_minter_types::QueryMsg as OpenEditionMinterQueryMsg; use omniflix_open_edition_minter::error::ContractError as OpenEditionMinterError; @@ -43,7 +34,7 @@ mod test_open_edition_minter_creation { ) = setup(); let admin = test_addresses.admin; let creator = test_addresses.creator; - let collector = test_addresses.collector; + let _collector = test_addresses.collector; // Instantiate the minter factory let open_edition_minter_factory_instantiate_msg = OpenEditionMinterFactoryInstantiateMsg { @@ -223,7 +214,7 @@ mod test_open_edition_minter_creation { let create_minter_msg = OpenEditionMinterFactoryExecuteMsg::CreateMinter { msg: open_edition_minter_instantiate_msg, }; - let res = app + let _res = app .execute_contract( creator.clone(), open_edition_minter_factory_address.clone(), @@ -361,5 +352,4 @@ mod test_open_edition_minter_creation { "description".to_string() ); } - // Query onft collection } diff --git a/integration-tests/src/test_open_edition_minting.rs b/integration-tests/src/test_open_edition_minting.rs new file mode 100644 index 0000000..46ce826 --- /dev/null +++ b/integration-tests/src/test_open_edition_minting.rs @@ -0,0 +1,288 @@ +#[cfg(test)] +mod test_open_edition_minter_minting { + + use cosmwasm_std::{coin, coins, Addr, BlockInfo, Coin, Timestamp, Uint128}; + + use cw_multi_test::{BankSudo, Executor, SudoMsg}; + use minter_types::UserDetails; + use omniflix_open_edition_minter_factory::msg::{ + ExecuteMsg as OpenEditionMinterFactoryExecuteMsg, + InstantiateMsg as OpenEditionMinterFactoryInstantiateMsg, + }; + + use crate::utils::{get_minter_address_from_res, return_open_edition_minter_inst_msg}; + + use crate::{setup::setup, utils::query_onft_collection}; + + use omniflix_open_edition_minter::msg::ExecuteMsg as OpenEditionMinterExecuteMsg; + + use open_edition_minter_types::QueryMsg as OpenEditionMinterQueryMsg; + + use omniflix_open_edition_minter::error::ContractError as OpenEditionMinterError; + + #[test] + fn test_open_edition_minting() { + let ( + mut app, + test_addresses, + _minter_factory_code_id, + _minter_code_id, + _round_whitelist_factory_code_id, + _round_whitelist_code_id, + open_edition_minter_factory_code_id, + open_edition_minter_code_id, + ) = setup(); + let admin = test_addresses.admin; + let creator = test_addresses.creator; + let collector = test_addresses.collector; + + // Instantiate the minter factory + let open_edition_minter_factory_instantiate_msg = OpenEditionMinterFactoryInstantiateMsg { + admin: Some(admin.to_string()), + allowed_minter_mint_denoms: vec!["uflix".to_string()], + open_edition_minter_code_id, + fee_collector_address: admin.to_string(), + minter_creation_fee: coin(1000000, "uflix"), + }; + + let open_edition_minter_factory_address = app + .instantiate_contract( + open_edition_minter_factory_code_id, + admin.clone(), + &open_edition_minter_factory_instantiate_msg, + &[], + "Open Edition Minter Factory", + None, + ) + .unwrap(); + + // Create a minter + let open_edition_minter_instantiate_msg = return_open_edition_minter_inst_msg(); + let create_minter_msg = OpenEditionMinterFactoryExecuteMsg::CreateMinter { + msg: open_edition_minter_instantiate_msg, + }; + + let res = app + .execute_contract( + creator.clone(), + open_edition_minter_factory_address, + &create_minter_msg, + &[Coin::new(2000000, "uflix")], + ) + .unwrap(); + let minter_address = get_minter_address_from_res(res); + + // Try minting before start time + let mint_msg = OpenEditionMinterExecuteMsg::Mint {}; + let res = app + .execute_contract( + collector.clone(), + Addr::unchecked(minter_address.clone()), + &mint_msg, + &[Coin::new(1000000, "uflix")], + ) + .unwrap_err(); + let err = res.source().unwrap(); + let error = err.downcast_ref::().unwrap(); + assert_eq!( + error, + &OpenEditionMinterError::MintingNotStarted { + start_time: Timestamp::from_nanos(1_000_000_000), + current_time: Timestamp::from_nanos(1_000) + } + ); + + // Try minting with incorrect payment amount + app.set_block(BlockInfo { + chain_id: "test_1".to_string(), + height: 1_000, + time: Timestamp::from_nanos(1_000_000_000), + }); + let mint_msg = OpenEditionMinterExecuteMsg::Mint {}; + let res = app + .execute_contract( + collector.clone(), + Addr::unchecked(minter_address.clone()), + &mint_msg, + &[Coin::new(1000000, "incorrect_denom")], + ) + .unwrap_err(); + let err = res.source().unwrap(); + let error = err.downcast_ref::().unwrap(); + assert_eq!( + error, + &OpenEditionMinterError::PaymentError(cw_utils::PaymentError::MissingDenom( + "uflix".to_string() + )) + ); + + // Try minting with incorrect payment amount + let mint_msg = OpenEditionMinterExecuteMsg::Mint {}; + let res = app + .execute_contract( + collector.clone(), + Addr::unchecked(minter_address.clone()), + &mint_msg, + &[Coin::new(1000000 - 1, "uflix")], + ) + .unwrap_err(); + let err = res.source().unwrap(); + let error = err.downcast_ref::().unwrap(); + assert_eq!( + error, + &OpenEditionMinterError::IncorrectPaymentAmount { + expected: Uint128::from(1000000u128), + sent: Uint128::from(999999u128) + } + ); + // Minting after end time + app.set_block(BlockInfo { + chain_id: "test_1".to_string(), + height: 1_000, + time: Timestamp::from_nanos(2_000_000_000 + 1), + }); + let mint_msg = OpenEditionMinterExecuteMsg::Mint {}; + + let res = app + .execute_contract( + collector.clone(), + Addr::unchecked(minter_address.clone()), + &mint_msg, + &[Coin::new(1000000, "uflix")], + ) + .unwrap_err(); + let err = res.source().unwrap(); + let error = err.downcast_ref::().unwrap(); + assert_eq!(error, &OpenEditionMinterError::PublicMintingEnded {}); + + // Set block time to valid minting time + app.set_block(BlockInfo { + chain_id: "test_1".to_string(), + height: 1_000, + time: Timestamp::from_nanos(1_000_000_000), + }); + + // Query uflix balance of creator before mint + let creator_balance_before_mint: Uint128 = app + .wrap() + .query_balance(creator.to_string(), "uflix".to_string()) + .unwrap() + .amount; + // Mint + let mint_msg = OpenEditionMinterExecuteMsg::Mint {}; + let _res = app + .execute_contract( + collector.clone(), + Addr::unchecked(minter_address.clone()), + &mint_msg, + &[Coin::new(1000000, "uflix")], + ) + .unwrap(); + // Query uflix balance of creator after mint + let creator_balance_after_mint: Uint128 = app + .wrap() + .query_balance(creator.to_string(), "uflix".to_string()) + .unwrap() + .amount; + // Check if creator got paid + assert_eq!( + creator_balance_after_mint, + creator_balance_before_mint + Uint128::from(1000000u128) + ); + + // Query minter + let query_msg = OpenEditionMinterQueryMsg::MintedTokens { + address: collector.to_string(), + }; + let res: UserDetails = app + .wrap() + .query_wasm_smart(Addr::unchecked(minter_address.clone()), &query_msg) + .unwrap(); + assert_eq!(res.total_minted_count, 1); + assert_eq!(res.minted_tokens[0].token_id, "1"); + + // Query onft collection + let collection = query_onft_collection(app.storage(), minter_address.clone()); + assert_eq!(collection.onfts.clone()[0].id, "1"); + assert_eq!( + collection.onfts.clone()[0].metadata.clone().unwrap().name, + "name".to_string() + ); + // Query minter + let query_msg = OpenEditionMinterQueryMsg::TokensRemaining {}; + let res: u32 = app + .wrap() + .query_wasm_smart(Addr::unchecked(minter_address.clone()), &query_msg) + .unwrap(); + assert_eq!(res, 999); + + // Query minter + let query_msg = OpenEditionMinterQueryMsg::TotalMintedCount {}; + let res: u32 = app + .wrap() + .query_wasm_smart(Addr::unchecked(minter_address.clone()), &query_msg) + .unwrap(); + assert_eq!(res, 1); + + // Create a loop from 1 to 999 and mint every remaining token to receivers + for i in 1..1000 { + let collector = Addr::unchecked(format!("collector{}", i)); + // Mint money for collector + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: collector.to_string(), + amount: coins(1000000, "uflix"), + })) + .unwrap(); + // Mint + let mint_msg = OpenEditionMinterExecuteMsg::Mint {}; + let _res = app + .execute_contract( + collector.clone(), + Addr::unchecked(minter_address.clone()), + &mint_msg, + &[Coin::new(1000000, "uflix")], + ) + .unwrap(); + + // Query minter + let query_msg = OpenEditionMinterQueryMsg::MintedTokens { + address: collector.to_string(), + }; + let res: UserDetails = app + .wrap() + .query_wasm_smart(Addr::unchecked(minter_address.clone()), &query_msg) + .unwrap(); + assert_eq!(res.total_minted_count, 1); + } + + // Query minter + let query_msg = OpenEditionMinterQueryMsg::TokensRemaining {}; + let res: u32 = app + .wrap() + .query_wasm_smart(Addr::unchecked(minter_address.clone()), &query_msg) + .unwrap(); + assert_eq!(res, 0); + + // Query minter + let query_msg = OpenEditionMinterQueryMsg::TotalMintedCount {}; + let res: u32 = app + .wrap() + .query_wasm_smart(Addr::unchecked(minter_address.clone()), &query_msg) + .unwrap(); + assert_eq!(res, 1000); + + // Try minting after all tokens are minted + let mint_msg = OpenEditionMinterExecuteMsg::Mint {}; + let res = app + .execute_contract( + collector.clone(), + Addr::unchecked(minter_address.clone()), + &mint_msg, + &[Coin::new(1000000, "uflix")], + ) + .unwrap_err(); + let err = res.source().unwrap(); + let error = err.downcast_ref::().unwrap(); + assert_eq!(error, &OpenEditionMinterError::NoTokensLeftToMint {}); + } +}