Repository: public-awesome/launchpad
Pull request #221 (2022-04-11 17:33:05)

random token list


Patch:
diff --git a/contracts/minter/schema/execute_msg.json b/contracts/minter/schema/execute_msg.json
index e0763c56..98763d66 100644
--- a/contracts/minter/schema/execute_msg.json
+++ b/contracts/minter/schema/execute_msg.json
@@ -114,6 +114,18 @@
       },
       "additionalProperties": false
     },
+    {
+      "type": "object",
+      "required": [
+        "shuffle"
+      ],
+      "properties": {
+        "shuffle": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    },
     {
       "type": "object",
       "required": [
diff --git a/contracts/minter/schema/query_msg.json b/contracts/minter/schema/query_msg.json
index c97025f7..6f83bb55 100644
--- a/contracts/minter/schema/query_msg.json
+++ b/contracts/minter/schema/query_msg.json
@@ -69,6 +69,18 @@
         }
       },
       "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "mintable_tokens"
+      ],
+      "properties": {
+        "mintable_tokens": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
     }
   ]
 }
diff --git a/contracts/minter/src/contract.rs b/contracts/minter/src/contract.rs
index d81597c3..33c28920 100644
--- a/contracts/minter/src/contract.rs
+++ b/contracts/minter/src/contract.rs
@@ -1,5 +1,13 @@
 use std::convert::TryInto;
 
+use crate::error::ContractError;
+use crate::msg::{
+    ConfigResponse, ExecuteMsg, InstantiateMsg, MintCountResponse, MintPriceResponse,
+    MintableNumTokensResponse, MintableTokensResponse, QueryMsg, StartTimeResponse,
+};
+use crate::state::{
+    Config, CONFIG, MINTABLE_NUM_TOKENS, MINTABLE_TOKEN_POSITIONS, MINTER_ADDRS, SG721_ADDRESS,
+};
 #[cfg(not(feature = "library"))]
 use cosmwasm_std::entry_point;
 use cosmwasm_std::{
@@ -9,22 +17,13 @@ use cosmwasm_std::{
 use cw2::set_contract_version;
 use cw721_base::{msg::ExecuteMsg as Cw721ExecuteMsg, MintMsg};
 use cw_utils::{may_pay, parse_reply_instantiate_data};
-use rand_core::SeedableRng;
+use rand_core::{RngCore, SeedableRng};
 use rand_xoshiro::Xoshiro128PlusPlus;
 use sg721::msg::InstantiateMsg as Sg721InstantiateMsg;
+use sg_std::{checked_fair_burn, StargazeMsgWrapper, GENESIS_MINT_START_TIME, NATIVE_DENOM};
 use sha2::{Digest, Sha256};
 use shuffle::{fy::FisherYates, shuffler::Shuffler};
 use url::Url;
-
-use crate::error::ContractError;
-use crate::msg::{
-    ConfigResponse, ExecuteMsg, InstantiateMsg, MintCountResponse, MintPriceResponse,
-    MintableNumTokensResponse, QueryMsg, StartTimeResponse,
-};
-use crate::state::{
-    Config, CONFIG, MINTABLE_NUM_TOKENS, MINTABLE_TOKEN_IDS, MINTER_ADDRS, SG721_ADDRESS,
-};
-use sg_std::{checked_fair_burn, StargazeMsgWrapper, GENESIS_MINT_START_TIME, NATIVE_DENOM};
 use whitelist::msg::{
     ConfigResponse as WhitelistConfigResponse, HasMemberResponse, QueryMsg as WhitelistQueryMsg,
 };
@@ -32,6 +31,11 @@ use whitelist::msg::{
 pub type Response = cosmwasm_std::Response<StargazeMsgWrapper>;
 pub type SubMsg = cosmwasm_std::SubMsg<StargazeMsgWrapper>;
 
+pub struct TokenPositionMapping {
+    pub position: u32,
+    pub token_id: u32,
+}
+
 // version info for migration info
 const CONTRACT_NAME: &str = "crates.io:sg-minter";
 const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -43,6 +47,7 @@ const MAX_TOKEN_LIMIT: u32 = 10000;
 const MAX_PER_ADDRESS_LIMIT: u32 = 50;
 const MIN_MINT_PRICE: u128 = 50_000_000;
 const MINT_FEE_PERCENT: u32 = 10;
+const SHUFFLE_FEE: u128 = 500_000_000;
 
 #[cfg_attr(not(feature = "library"), entry_point)]
 pub fn instantiate(
@@ -123,9 +128,16 @@ pub fn instantiate(
     CONFIG.save(deps.storage, &config)?;
     MINTABLE_NUM_TOKENS.save(deps.storage, &msg.num_tokens)?;
 
+    let token_ids = random_token_list(
+        &env,
+        info.sender.clone(),
+        (1..=msg.num_tokens).collect::<Vec<u32>>(),
+    )?;
     // Save mintable token ids map
-    for token_id in 1..=msg.num_tokens {
-        MINTABLE_TOKEN_IDS.save(deps.storage, token_id, &true)?;
+    let mut token_position = 1;
+    for token_id in token_ids {
+        MINTABLE_TOKEN_POSITIONS.save(deps.storage, token_position, &token_id)?;
+        token_position += 1;
     }
 
     // Submessage to instantiate sg721 contract
@@ -177,10 +189,46 @@ pub fn execute(
         ExecuteMsg::SetWhitelist { whitelist } => {
             execute_set_whitelist(deps, env, info, &whitelist)
         }
+        ExecuteMsg::Shuffle {} => execute_shuffle(deps, env, info),
         ExecuteMsg::Withdraw {} => execute_withdraw(deps, env, info),
     }
 }
 
+// Anyone can pay to shuffle at any time
+// Introduces another source of randomness to minting
+// There's a fee because this action is expensive.
+pub fn execute_shuffle(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+) -> Result<Response, ContractError> {
+    // Check exact shuffle fee payment included in message
+    let fee_msgs = checked_fair_burn(&info, SHUFFLE_FEE)?;
+    // Check not sold out
+    let mintable_num_tokens = MINTABLE_NUM_TOKENS.load(deps.storage)?;
+    if mintable_num_tokens == 0 {
+        return Err(ContractError::SoldOut {});
+    }
+
+    // get positions and token_ids, then randomize token_ids and reassign positions
+    let mut positions = vec![];
+    let mut token_ids = vec![];
+    for mapping in MINTABLE_TOKEN_POSITIONS.range(deps.storage, None, None, Order::Ascending) {
+        let (position, token_id) = mapping?;
+        positions.push(position);
+        token_ids.push(token_id);
+    }
+    let randomized_token_ids = random_token_list(&env, info.sender.clone(), token_ids.clone())?;
+    for (i, position) in positions.iter().enumerate() {
+        MINTABLE_TOKEN_POSITIONS.save(deps.storage, *position, &randomized_token_ids[i])?;
+    }
+
+    Ok(Response::default()
+        .add_attribute("action", "shuffle")
+        .add_attribute("sender", info.sender)
+        .add_messages(fee_msgs))
+}
+
 pub fn execute_withdraw(
     deps: DepsMut,
     env: Env,
@@ -373,15 +421,19 @@ fn _execute_mint(
     recipient: Option<Addr>,
     token_id: Option<u32>,
 ) -> Result<Response, ContractError> {
-    let mintable = MINTABLE_TOKEN_IDS
-        .keys(deps.storage, None, None, Order::Ascending)
-        .next()
-        .is_some();
-    if !mintable {
+    let mintable_num_tokens = MINTABLE_NUM_TOKENS.load(deps.storage)?;
+    if mintable_num_tokens == 0 {
         return Err(ContractError::SoldOut {});
     }
 
     let config = CONFIG.load(deps.storage)?;
+
+    if let Some(token_id) = token_id {
+        if token_id == 0 || token_id > config.num_tokens {
+            return Err(ContractError::InvalidTokenId {});
+        }
+    }
+
     let sg721_address = SG721_ADDRESS.load(deps.storage)?;
 
     let recipient_addr = match recipient {
@@ -411,28 +463,35 @@ fn _execute_mint(
         network_fee
     };
 
-    let mintable_token_id = match token_id {
+    let mintable_token_mapping = match token_id {
         Some(token_id) => {
-            if token_id == 0 || token_id > config.num_tokens {
-                return Err(ContractError::InvalidTokenId {});
+            // set position to invalid value, iterate to find matching token_id
+            // if token_id not found, token_id is already sold, position is unchanged and throw err
+            // otherwise return position and token_id
+            let mut position = 0;
+            for res in MINTABLE_TOKEN_POSITIONS.range(deps.storage, None, None, Order::Ascending) {
+                let (pos, id) = res?;
+                if id == token_id {
+                    position = pos;
+                    break;
+                }
             }
-            // If token_id not on mintable map, throw err
-            if !MINTABLE_TOKEN_IDS.has(deps.storage, token_id) {
+            if position == 0 {
                 return Err(ContractError::TokenIdAlreadySold { token_id });
             }
-            token_id
-        }
-        None => {
-            let token_id: u32 = random_mintable_token_id(deps.as_ref(), env, info.sender.clone())?;
-            token_id
+            TokenPositionMapping { position, token_id }
         }
+        None => random_mintable_token_mapping(deps.as_ref(), env, info.sender.clone())?,
     };
 
     // Create mint msgs
     let mint_msg = Cw721ExecuteMsg::Mint(MintMsg::<Empty> {
-        token_id: mintable_token_id.to_string(),
+        token_id: mintable_token_mapping.token_id.to_string(),
         owner: recipient_addr.to_string(),
-        token_uri: Some(format!("{}/{}", config.base_token_uri, mintable_token_id)),
+        token_uri: Some(format!(
+            "{}/{}",
+            config.base_token_uri, mintable_token_mapping.token_id
+        )),
         extension: Empty {},
     });
     let msg = CosmosMsg::Wasm(WasmMsg::Execute {
@@ -442,8 +501,8 @@ fn _execute_mint(
     });
     msgs.append(&mut vec![msg]);
 
-    // Remove mintable token id from map
-    MINTABLE_TOKEN_IDS.remove(deps.storage, mintable_token_id);
+    // Remove mintable token position from map
+    MINTABLE_TOKEN_POSITIONS.remove(deps.storage, mintable_token_mapping.position);
     let mintable_num_tokens = MINTABLE_NUM_TOKENS.load(deps.storage)?;
     // Decrement mintable num tokens
     MINTABLE_NUM_TOKENS.save(deps.storage, &(mintable_num_tokens - 1))?;
@@ -455,38 +514,62 @@ fn _execute_mint(
         .add_attribute("action", action)
         .add_attribute("sender", info.sender)
         .add_attribute("recipient", recipient_addr)
-        .add_attribute("token_id", mintable_token_id.to_string())
+        .add_attribute("token_id", mintable_token_mapping.token_id.to_string())
         .add_attribute("network_fee", network_fee)
         .add_attribute("mint_price", mint_price.amount)
         .add_messages(msgs))
 }
 
-fn random_mintable_token_id(deps: Deps, env: Env, sender: Addr) -> Result<u32, ContractError> {
-    let tx_index = env
-        .transaction
-        .map(|tx| tx.index)
-        .ok_or(ContractError::NoEnvTransactionIndex {})?;
-
-    let sha256 = Sha256::digest(format!("{}{}{}", sender, tx_index, env.block.height).into_bytes());
+fn random_token_list(
+    env: &Env,
+    sender: Addr,
+    mut tokens: Vec<u32>,
+) -> Result<Vec<u32>, ContractError> {
+    let sha256 =
+        Sha256::digest(format!("{}{}{}", sender, env.block.height, tokens.len()).into_bytes());
     // Cut first 16 bytes from 32 byte value
     let randomness: [u8; 16] = sha256.to_vec()[0..16].try_into().unwrap();
-
-    let mut mintable_tokens = MINTABLE_TOKEN_IDS
-        .keys(deps.storage, None, None, Order::Ascending)
-        .collect::<StdResult<Vec<_>>>()?;
-
-    // See https://docs.rs/rand/0.8.5/rand/rngs/struct.SmallRng.html
-    // where this is used for 32 bit systems.
-    // We don't use the SmallRng in order to get the same implementation
-    // in unit tests (64 bit dev machines) and the real contract (32 bit Wasm)
     let mut rng = Xoshiro128PlusPlus::from_seed(randomness);
-
     let mut shuffler = FisherYates::default();
     shuffler
-        .shuffle(&mut mintable_tokens, &mut rng)
+        .shuffle(&mut tokens, &mut rng)
         .map_err(StdError::generic_err)?;
+    Ok(tokens)
+}
+
+// Does a baby shuffle, picking a token_id from the first or last 50 mintable positions.
+fn random_mintable_token_mapping(
+    deps: Deps,
+    env: Env,
+    sender: Addr,
+) -> Result<TokenPositionMapping, ContractError> {
+    let num_tokens = MINTABLE_NUM_TOKENS.load(deps.storage)?;
+    let sha256 =
+        Sha256::digest(format!("{}{}{}", sender, num_tokens, env.block.height).into_bytes());
+    // Cut first 16 bytes from 32 byte value
+    let randomness: [u8; 16] = sha256.to_vec()[0..16].try_into().unwrap();
+
+    let mut rng = Xoshiro128PlusPlus::from_seed(randomness);
+
+    let r = rng.next_u32();
 
-    Ok(mintable_tokens[0])
+    let order = match r % 2 {
+        1 => Order::Descending,
+        _ => Order::Ascending,
+    };
+    let mut rem = 50;
+    if rem > num_tokens {
+        rem = num_tokens;
+    }
+    let n = r % rem;
+    let position = MINTABLE_TOKEN_POSITIONS
+        .keys(deps.storage, None, None, order)
+        .skip(n as usize)
+        .take(1)
+        .collect::<StdResult<Vec<_>>>()?[0];
+
+    let token_id = MINTABLE_TOKEN_POSITIONS.load(deps.storage, position)?;
+    Ok(TokenPositionMapping { position, token_id })
 }
 
 pub fn execute_update_start_time(
@@ -595,6 +678,8 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
         QueryMsg::MintableNumTokens {} => to_binary(&query_mintable_num_tokens(deps)?),
         QueryMsg::MintPrice {} => to_binary(&query_mint_price(deps)?),
         QueryMsg::MintCount { address } => to_binary(&query_mint_count(deps, address)?),
+        // TODO for debug to test shuffle. remove before prod
+        QueryMsg::MintableTokens {} => to_binary(&query_mintable_tokens(deps)?),
     }
 }
 
@@ -636,6 +721,18 @@ fn query_mintable_num_tokens(deps: Deps) -> StdResult<MintableNumTokensResponse>
     Ok(MintableNumTokensResponse { count })
 }
 
+//TODO for debug to test shuffle. remove before prod
+fn query_mintable_tokens(deps: Deps) -> StdResult<MintableTokensResponse> {
+    let tokens = MINTABLE_TOKEN_POSITIONS
+        .range(deps.storage, None, None, Order::Ascending)
+        .map(|t| t.unwrap())
+        .collect::<Vec<_>>();
+
+    Ok(MintableTokensResponse {
+        mintable_tokens: tokens,
+    })
+}
+
 fn query_mint_price(deps: Deps) -> StdResult<MintPriceResponse> {
     let config = CONFIG.load(deps.storage)?;
     let current_price = mint_price(deps, false)?;
diff --git a/contracts/minter/src/msg.rs b/contracts/minter/src/msg.rs
index b85c4ef4..513c9da7 100644
--- a/contracts/minter/src/msg.rs
+++ b/contracts/minter/src/msg.rs
@@ -25,6 +25,7 @@ pub enum ExecuteMsg {
     UpdatePerAddressLimit { per_address_limit: u32 },
     MintTo { recipient: String },
     MintFor { token_id: u32, recipient: String },
+    Shuffle {},
     Withdraw {},
 }
 
@@ -36,6 +37,7 @@ pub enum QueryMsg {
     StartTime {},
     MintPrice {},
     MintCount { address: String },
+    MintableTokens {},
 }
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@@ -73,3 +75,9 @@ pub struct MintCountResponse {
     pub address: String,
     pub count: u32,
 }
+
+//TODO for debug to test shuffle. remove before prod
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct MintableTokensResponse {
+    pub mintable_tokens: Vec<(u32, u32)>,
+}
diff --git a/contracts/minter/src/state.rs b/contracts/minter/src/state.rs
index d957d083..0e64f4c8 100644
--- a/contracts/minter/src/state.rs
+++ b/contracts/minter/src/state.rs
@@ -18,6 +18,7 @@ pub struct Config {
 
 pub const CONFIG: Item<Config> = Item::new("config");
 pub const SG721_ADDRESS: Item<Addr> = Item::new("sg721_address");
-pub const MINTABLE_TOKEN_IDS: Map<u32, bool> = Map::new("mt");
+// map of index position and token id
+pub const MINTABLE_TOKEN_POSITIONS: Map<u32, u32> = Map::new("mt");
 pub const MINTABLE_NUM_TOKENS: Item<u32> = Item::new("mintable_num_tokens");
 pub const MINTER_ADDRS: Map<Addr, u32> = Map::new("ma");
diff --git a/packages/sg-utils/src/bech32_convert.rs b/packages/sg-utils/src/bech32_convert.rs
index 1f729efe..039e3a74 100644
--- a/packages/sg-utils/src/bech32_convert.rs
+++ b/packages/sg-utils/src/bech32_convert.rs
@@ -23,7 +23,7 @@ impl ToStars for str {
 
 impl ToStars for Addr {
     fn to_stars(&self) -> Result<String, Error> {
-        let decoded = decode_and_convert(&self.to_string())?;
+        let decoded = decode_and_convert(self.as_ref())?;
         convert_and_encode(PREFIX.to_string(), decoded.1)
     }
 }


Test patch:
diff --git a/contracts/minter/src/contract_tests.rs b/contracts/minter/src/contract_tests.rs
index 14f823f5..e60a865e 100644
--- a/contracts/minter/src/contract_tests.rs
+++ b/contracts/minter/src/contract_tests.rs
@@ -14,7 +14,7 @@ use whitelist::msg::{AddMembersMsg, ExecuteMsg as WhitelistExecuteMsg};
 use crate::contract::instantiate;
 use crate::msg::{
     ConfigResponse, ExecuteMsg, InstantiateMsg, MintCountResponse, MintPriceResponse,
-    MintableNumTokensResponse, QueryMsg, StartTimeResponse,
+    MintableNumTokensResponse, MintableTokensResponse, QueryMsg, StartTimeResponse,
 };
 use crate::ContractError;
 
@@ -435,9 +435,9 @@ fn happy_path() {
     assert_eq!(res.address, buyer.to_string());
 
     // Check NFT owned by buyer
-    // Random mint token_id 2
+    // Random mint token_id 1
     let query_owner_msg = Cw721QueryMsg::OwnerOf {
-        token_id: String::from("2"),
+        token_id: String::from("1"),
         include_expired: None,
     };
     let res: OwnerOfResponse = router
@@ -649,11 +649,24 @@ fn mint_count_query() {
     assert_eq!(res.count, 2);
     assert_eq!(res.address, buyer.to_string());
 
+    // get random mint token_id
+    let tokens_msg = Cw721QueryMsg::Tokens {
+        owner: buyer.to_string(),
+        start_after: None,
+        limit: None,
+    };
+    let res: TokensResponse = router
+        .wrap()
+        .query_wasm_smart(sg721_addr.clone(), &tokens_msg)
+        .unwrap();
+    let sold_token_id: u32 = res.tokens[1].parse::<u32>().unwrap();
+    println!("sold token id: {}", sold_token_id);
+
     // Buyer transfers NFT to creator
-    // random mint token id: 4
+    // random mint token id: 8
     let transfer_msg: Cw721ExecuteMsg<Empty> = Cw721ExecuteMsg::TransferNft {
         recipient: creator.to_string(),
-        token_id: "4".to_string(),
+        token_id: "8".to_string(),
     };
     let res = router.execute_contract(
         buyer.clone(),
@@ -1117,7 +1130,7 @@ fn mint_for_token_id_addr() {
     };
     let res: TokensResponse = router
         .wrap()
-        .query_wasm_smart(config.sg721_address, &tokens_msg)
+        .query_wasm_smart(config.sg721_address.clone(), &tokens_msg)
         .unwrap();
     let sold_token_id: u32 = res.tokens[0].parse::<u32>().unwrap();
 
@@ -1198,6 +1211,18 @@ fn mint_for_token_id_addr() {
     );
     assert!(res.is_ok());
 
+    let res: OwnerOfResponse = router
+        .wrap()
+        .query_wasm_smart(
+            config.sg721_address,
+            &Cw721QueryMsg::OwnerOf {
+                token_id: 2.to_string(),
+                include_expired: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(res.owner, buyer.to_string());
+
     let mintable_num_tokens_response: MintableNumTokensResponse = router
         .wrap()
         .query_wasm_smart(minter_addr, &QueryMsg::MintableNumTokens {})
@@ -1504,3 +1529,94 @@ fn can_withdraw() {
         coins(INITIAL_BALANCE + UNIT_PRICE - MINT_FEE, NATIVE_DENOM)
     );
 }
+
+//TODO for debug to test shuffle. remove before prod
+#[test]
+fn shuffle() {
+    const SHUFFLE_FEE: u128 = 500_000_000;
+    // setup accounts
+    let mut router = custom_mock_app();
+    let (creator, buyer) = setup_accounts(&mut router);
+    // setup contracts
+    let num_tokens = 20;
+    let (minter_addr, _config) = setup_minter_contract(&mut router, &creator, num_tokens);
+    // query mintable order for mints
+    let query_mintable_tokens_msg = QueryMsg::MintableTokens {};
+    let res: MintableTokensResponse = router
+        .wrap()
+        .query_wasm_smart(minter_addr.clone(), &query_mintable_tokens_msg)
+        .unwrap();
+    dbg!("after initialize {:?}", res);
+    // perform shuffle
+    let shuffle_msg = ExecuteMsg::Shuffle {};
+    let funds = coins(SHUFFLE_FEE, NATIVE_DENOM);
+    let res = router.execute_contract(creator.clone(), minter_addr.clone(), &shuffle_msg, &funds);
+    assert!(res.is_ok());
+    // query and compare mintable order for mints
+    let query_mintable_tokens_msg = QueryMsg::MintableTokens {};
+    let res: MintableTokensResponse = router
+        .wrap()
+        .query_wasm_smart(minter_addr.clone(), &query_mintable_tokens_msg)
+        .unwrap();
+    dbg!("after shuffle {:?}", res);
+    // mint a few tokens
+    let mut i = 0;
+    while i < 3 {
+        let mint_to_msg = ExecuteMsg::MintTo {
+            recipient: buyer.to_string(),
+        };
+        let res = router.execute_contract(
+            creator.clone(),
+            minter_addr.clone(),
+            &mint_to_msg,
+            &coins_for_msg(Coin {
+                amount: Uint128::from(ADMIN_MINT_PRICE),
+                denom: NATIVE_DENOM.to_string(),
+            }),
+        );
+        assert!(res.is_ok());
+        i += 1;
+    }
+    println!("---after mint_to---");
+    // query mintable order for mints
+    let query_mintable_tokens_msg = QueryMsg::MintableTokens {};
+    let res: MintableTokensResponse = router
+        .wrap()
+        .query_wasm_smart(minter_addr.clone(), &query_mintable_tokens_msg)
+        .unwrap();
+    dbg!("{:?}", res);
+    // perform shuffle
+    let shuffle_msg = ExecuteMsg::Shuffle {};
+    let funds = coins(SHUFFLE_FEE, NATIVE_DENOM);
+    let res = router.execute_contract(creator.clone(), minter_addr.clone(), &shuffle_msg, &funds);
+    assert!(res.is_ok());
+    // query and compare mintable order for mints
+    let query_mintable_tokens_msg = QueryMsg::MintableTokens {};
+    let res: MintableTokensResponse = router
+        .wrap()
+        .query_wasm_smart(minter_addr.clone(), &query_mintable_tokens_msg)
+        .unwrap();
+    dbg!("after another shuffle {:?}", res);
+    // mint until sold out
+    while i < num_tokens {
+        let mint_to_msg = ExecuteMsg::MintTo {
+            recipient: buyer.to_string(),
+        };
+        let res = router.execute_contract(
+            creator.clone(),
+            minter_addr.clone(),
+            &mint_to_msg,
+            &coins_for_msg(Coin {
+                amount: Uint128::from(ADMIN_MINT_PRICE),
+                denom: NATIVE_DENOM.to_string(),
+            }),
+        );
+        assert!(res.is_ok());
+        i += 1;
+    }
+    // try shuffle
+    let shuffle_msg = ExecuteMsg::Shuffle {};
+    let funds = coins(SHUFFLE_FEE, NATIVE_DENOM);
+    let res = router.execute_contract(creator.clone(), minter_addr, &shuffle_msg, &funds);
+    assert!(res.is_err());
+}