diff --git a/contracts/minters/vending-minter/src/contract.rs b/contracts/minters/vending-minter/src/contract.rs index 2f649743a..43a51f081 100644 --- a/contracts/minters/vending-minter/src/contract.rs +++ b/contracts/minters/vending-minter/src/contract.rs @@ -21,7 +21,7 @@ use cw721_base::Extension; use cw_utils::{may_pay, maybe_addr, nonpayable, parse_reply_instantiate_data}; use semver::Version; -use sg1::distribute_mint_fees; +use sg1::{checked_fair_burn, distribute_mint_fees}; use sg2::query::Sg2QueryMsg; use sg4::{MinterConfig, Status, StatusResponse, SudoMsg}; use sg721::{ExecuteMsg as Sg721ExecuteMsg, InstantiateMsg as Sg721InstantiateMsg}; @@ -31,7 +31,6 @@ use sg_whitelist::msg::{ ConfigResponse as WhitelistConfigResponse, HasMemberResponse, QueryMsg as WhitelistQueryMsg, }; use sha2::{Digest, Sha256}; - use url::Url; use vending_factory::msg::{ParamsResponse, VendingMinterCreateMsg}; use vending_factory::state::VendingMinterParams; @@ -342,42 +341,49 @@ pub fn execute_purge( // Introduces another source of randomness to minting // There's a fee because this action is expensive. pub fn execute_shuffle( - _deps: DepsMut, - _env: Env, + deps: DepsMut, + env: Env, info: MessageInfo, ) -> Result { - let res = Response::new(); - - // let config = CONFIG.load(deps.storage)?; - - // let factory: ParamsResponse = deps - // .querier - // .query_wasm_smart(config.factory, &Sg2QueryMsg::Params {})?; - // let factory_params = factory.params; - - // // Check exact shuffle fee payment included in message - // checked_fair_burn( - // &info, - // factory_params.extension.shuffle_fee.amount.u128(), - // None, - // &mut res, - // )?; - - // // 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 mut res = Response::new(); + + let config = CONFIG.load(deps.storage)?; + let factory: ParamsResponse = deps + .querier + .query_wasm_smart(config.factory, &Sg2QueryMsg::Params {})?; + let factory_params = factory.params; + + // Check exact shuffle fee payment included in message + checked_fair_burn( + &info, + factory_params.extension.shuffle_fee.amount.u128(), + None, + &mut res, + )?; + + // Check not sold out + let mintable_num_tokens = MINTABLE_NUM_TOKENS.load(deps.storage)?; + if mintable_num_tokens == 0 { + return Err(ContractError::SoldOut {}); + } + + let sha256 = Sha256::digest( + format!( + "{}{}{}{}{}", + info.sender, + env.block.height, + mintable_num_tokens, + env.block.time.nanos(), + env.transaction.map_or(0, |tx| tx.index), + ) + .into_bytes(), + ); + let randomness: [u8; 32] = sha256.to_vec()[0..32].try_into().map_err(|_| { + ContractError::Std(StdError::generic_err("Failed to convert sha256 to array")) + })?; + + sg_minter_utils::shuffle(deps.storage, randomness)?; Ok(res .add_attribute("action", "shuffle") .add_attribute("sender", info.sender)) @@ -750,70 +756,6 @@ fn _execute_mint( )) } -// fn random_token_list( -// env: &Env, -// sender: Addr, -// mut tokens: Vec, -// ) -> Result, ContractError> { -// let tx_index = if let Some(tx) = &env.transaction { -// tx.index -// } else { -// 0 -// }; -// let sha256 = Sha256::digest( -// format!("{}{}{}{}", sender, env.block.height, tokens.len(), tx_index).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 mut shuffler = FisherYates::default(); -// shuffler -// .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 { -// let num_tokens = MINTABLE_NUM_TOKENS.load(deps.storage)?; -// let tx_index = if let Some(tx) = &env.transaction { -// tx.index -// } else { -// 0 -// }; -// let sha256 = Sha256::digest( -// format!("{}{}{}{}", sender, num_tokens, env.block.height, tx_index).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(); - -// 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::>>()?[0]; - -// let token_id = MINTABLE_TOKEN_POSITIONS.load(deps.storage, position)?; -// Ok(TokenPositionMapping { position, token_id }) -// } - pub fn execute_update_mint_price( deps: DepsMut, env: Env, @@ -1019,17 +961,9 @@ pub fn execute_burn_remaining( if mintable_num_tokens == 0 { return Err(ContractError::SoldOut {}); } - - // TODO: implement purge in sg_minter_utils - // let keys = MINTABLE_TOKEN_POSITIONS - // .keys(deps.storage, None, None, Order::Ascending) - // .collect::>(); - // let mut total: u32 = 0; - // for key in keys { - // total += 1; - // MINTABLE_TOKEN_POSITIONS.remove(deps.storage, key?); - // } - // Decrement mintable num tokens + // purge all buckets + sg_minter_utils::purge_buckets(deps.storage, sg_minter_utils::MAX_BUCKETS)?; + // Set mintable num tokens to 0 MINTABLE_NUM_TOKENS.save(deps.storage, &0)?; let event = Event::new("burn-remaining") diff --git a/packages/sg-minter-utils/src/lib.rs b/packages/sg-minter-utils/src/lib.rs index e7642cbe3..a07527980 100644 --- a/packages/sg-minter-utils/src/lib.rs +++ b/packages/sg-minter-utils/src/lib.rs @@ -2,12 +2,12 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{StdError, Storage}; use cw_storage_plus::Item; -use nois::{int_in_range, sub_randomness_with_key}; +use nois::{int_in_range, shuffle as nois_shuffle, sub_randomness_with_key}; use thiserror::Error; // BUCKET_SIZE is limited to 256 to efficiently store ids as u8 partitioning into multiple buckets -const BUCKET_SIZE: u32 = 256; - -const MAX_SIZE: u32 = 256 * 256; +pub const BUCKET_SIZE: u32 = 256; +pub const MAX_BUCKETS: u32 = 256; +pub const MAX_SIZE: u32 = MAX_BUCKETS * BUCKET_SIZE; // buckets returns the number of necessary buckets for a given collection size. // it returns the number of buckets and the size of the last bucket @@ -159,6 +159,16 @@ pub fn purge_buckets(storage: &mut dyn Storage, max_buckets: u32) -> Result Result<(), MinterUtilsError> { + let mut provider = sub_randomness_with_key(random_seed, b"shuffle"); + let Some(available_buckets) = storage.get(&AVAILABLE_BUCKETS_KEY) else { + return Err(MinterUtilsError::NoAvailableBuckets {}); + }; + let shuffled = nois_shuffle(provider.provide(), available_buckets); + storage.set(&AVAILABLE_BUCKETS_KEY, &shuffled); + Ok(()) +} + pub fn pick_token(storage: &mut dyn Storage, token_id: u32) -> Result { let Some(mut available_buckets) = storage.get(&AVAILABLE_BUCKETS_KEY) else { return Err(MinterUtilsError::NoAvailableBuckets {}); @@ -166,7 +176,8 @@ pub fn pick_token(storage: &mut dyn Storage, token_id: u32) -> Result Result>() + ); + assert_eq!( + available_buckets, + [ + 21, 30, 37, 23, 11, 2, 29, 27, 8, 0, 7, 5, 15, 17, 6, 32, 25, 9, 36, 26, 13, 31, + 24, 10, 39, 35, 33, 12, 20, 16, 28, 18, 34, 19, 1, 4, 38, 22, 14, 3 + ] + ); + } + + #[test] + fn test_shuffle_and_pick_token() { + let mut deps = mock_dependencies(); + + let r = initialize(&mut deps.storage, 10_000); + assert!(r.is_ok()); + let r = shuffle(&mut deps.storage, [0; 32]); + assert!(r.is_ok()); + let token_id = pick_token(&mut deps.storage, 975).unwrap(); + assert_eq!(token_id, 975); + } }