open_edition_minter/
contract.rs

1use crate::error::ContractError;
2use crate::helpers::mint_nft_msg;
3use crate::msg::{
4    ConfigResponse, EndTimeResponse, ExecuteMsg, MintCountResponse, MintPriceResponse,
5    MintableNumTokensResponse, QueryMsg, StartTimeResponse, TotalMintCountResponse,
6};
7use crate::state::{
8    increment_token_index, Config, ConfigExtension, CONFIG, MINTABLE_NUM_TOKENS, MINTER_ADDRS,
9    SG721_ADDRESS, STATUS, TOTAL_MINT_COUNT,
10};
11#[cfg(not(feature = "library"))]
12use cosmwasm_std::entry_point;
13use cosmwasm_std::{
14    coin, ensure, to_json_binary, Addr, BankMsg, Binary, Coin, Decimal, Deps, DepsMut, Empty, Env,
15    Event, MessageInfo, Order, Reply, ReplyOn, StdError, StdResult, Timestamp, WasmMsg,
16};
17use cw2::set_contract_version;
18use cw_utils::{may_pay, maybe_addr, nonpayable, parse_reply_instantiate_data};
19use open_edition_factory::msg::{OpenEditionMinterCreateMsg, ParamsResponse};
20use open_edition_factory::state::OpenEditionMinterParams;
21use open_edition_factory::types::NftMetadataType;
22use semver::Version;
23use sg1::distribute_mint_fees;
24use sg2::query::Sg2QueryMsg;
25use sg4::{MinterConfig, Status, StatusResponse, SudoMsg};
26use sg721::{ExecuteMsg as Sg721ExecuteMsg, InstantiateMsg as Sg721InstantiateMsg};
27use sg_std::StargazeMsgWrapper;
28use sg_whitelist::msg::{
29    ConfigResponse as WhitelistConfigResponse, HasMemberResponse, QueryMsg as WhitelistQueryMsg,
30};
31use url::Url;
32
33pub type Response = cosmwasm_std::Response<StargazeMsgWrapper>;
34pub type SubMsg = cosmwasm_std::SubMsg<StargazeMsgWrapper>;
35
36// version info for migration info
37const CONTRACT_NAME: &str = "crates.io:sg-open-edition-minter";
38const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
39
40const INSTANTIATE_SG721_REPLY_ID: u64 = 1;
41
42#[cfg_attr(not(feature = "library"), entry_point)]
43pub fn instantiate(
44    deps: DepsMut,
45    env: Env,
46    info: MessageInfo,
47    mut msg: OpenEditionMinterCreateMsg,
48) -> Result<Response, ContractError> {
49    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
50
51    let factory = info.sender.clone();
52
53    // Make sure the sender is the factory contract
54    // This will fail if the sender cannot parse a response from the factory contract
55    let factory_response: ParamsResponse = deps
56        .querier
57        .query_wasm_smart(factory.clone(), &Sg2QueryMsg::Params {})?;
58    let factory_params = factory_response.params;
59
60    // set default status so it can be queried without failing
61    STATUS.save(deps.storage, &Status::default())?;
62
63    match msg.init_msg.nft_data.nft_data_type {
64        // If off-chain metadata -> Sanitize base token uri
65        NftMetadataType::OffChainMetadata => {
66            let base_token_uri = msg
67                .init_msg
68                .nft_data
69                .token_uri
70                .as_ref()
71                .map(|uri| uri.trim().to_string())
72                .map_or_else(|| Err(ContractError::InvalidBaseTokenURI {}), Ok)?;
73            // Token URI must be a valid URL (ipfs, https, etc.)
74            Url::parse(&base_token_uri).map_err(|_| ContractError::InvalidBaseTokenURI {})?;
75            msg.init_msg.nft_data.token_uri = Some(base_token_uri);
76        }
77        // If on-chain metadata -> make sure that the image data is a valid URL
78        NftMetadataType::OnChainMetadata => {
79            let base_img_url = msg
80                .init_msg
81                .nft_data
82                .extension
83                .as_ref()
84                .and_then(|ext| ext.image.as_ref().map(|img| img.trim()))
85                .map(Url::parse)
86                .transpose()?
87                .map(|url| url.to_string());
88            if let Some(ext) = msg.init_msg.nft_data.extension.as_mut() {
89                ext.image = base_img_url;
90            }
91        }
92    }
93
94    // Validations/Check at the factory level:
95    // - Mint price, # of tokens / address, Start & End time, Max Tokens
96
97    // Validate address for the optional whitelist contract
98    let whitelist_addr = msg
99        .init_msg
100        .whitelist
101        .and_then(|w| deps.api.addr_validate(w.as_str()).ok());
102
103    if let Some(whitelist) = whitelist_addr.clone() {
104        // check the whitelist exists
105        let res: WhitelistConfigResponse = deps
106            .querier
107            .query_wasm_smart(whitelist, &WhitelistQueryMsg::Config {})?;
108        if res.is_active {
109            return Err(ContractError::WhitelistAlreadyStarted {});
110        }
111    }
112
113    // Use default start trading time if not provided
114    let mut collection_info = msg.collection_params.info.clone();
115    let offset = factory_params.max_trading_offset_secs;
116    let default_start_time_with_offset = msg.init_msg.start_time.plus_seconds(offset);
117    if let Some(start_trading_time) = msg.collection_params.info.start_trading_time {
118        // If trading start time > start_time + offset, return error
119        if start_trading_time > default_start_time_with_offset {
120            return Err(ContractError::InvalidStartTradingTime(
121                start_trading_time,
122                default_start_time_with_offset,
123            ));
124        }
125    }
126    let start_trading_time = msg
127        .collection_params
128        .info
129        .start_trading_time
130        .or(Some(default_start_time_with_offset));
131    collection_info.start_trading_time = start_trading_time;
132
133    let config = Config {
134        factory: factory.clone(),
135        collection_code_id: msg.collection_params.code_id,
136        extension: ConfigExtension {
137            admin: deps
138                .api
139                .addr_validate(&msg.collection_params.info.creator)?,
140            payment_address: maybe_addr(deps.api, msg.init_msg.payment_address)?,
141            per_address_limit: msg.init_msg.per_address_limit,
142            start_time: msg.init_msg.start_time,
143            end_time: msg.init_msg.end_time,
144            nft_data: msg.init_msg.nft_data,
145            num_tokens: msg.init_msg.num_tokens,
146            whitelist: whitelist_addr,
147        },
148        mint_price: msg.init_msg.mint_price,
149    };
150
151    CONFIG.save(deps.storage, &config)?;
152
153    // Init the minted tokens count
154    TOTAL_MINT_COUNT.save(deps.storage, &0)?;
155
156    // Max token count (optional)
157    if let Some(max_num_tokens) = msg.init_msg.num_tokens {
158        MINTABLE_NUM_TOKENS.save(deps.storage, &max_num_tokens)?;
159    } else {
160        MINTABLE_NUM_TOKENS.save(deps.storage, &factory_params.extension.max_token_limit)?;
161    }
162
163    // Submessage to instantiate sg721 contract
164    let submsg = SubMsg {
165        msg: WasmMsg::Instantiate {
166            code_id: msg.collection_params.code_id,
167            msg: to_json_binary(&Sg721InstantiateMsg {
168                name: msg.collection_params.name.clone(),
169                symbol: msg.collection_params.symbol,
170                minter: env.contract.address.to_string(),
171                collection_info,
172            })?,
173            funds: info.funds,
174            admin: Some(config.extension.admin.to_string()),
175            label: format!("SG721-{}", msg.collection_params.name.trim()),
176        }
177        .into(),
178        id: INSTANTIATE_SG721_REPLY_ID,
179        gas_limit: None,
180        reply_on: ReplyOn::Success,
181    };
182
183    Ok(Response::new()
184        .add_attribute("action", "instantiate")
185        .add_attribute("contract_name", CONTRACT_NAME)
186        .add_attribute("contract_version", CONTRACT_VERSION)
187        .add_attribute("sender", factory)
188        .add_submessage(submsg))
189}
190
191#[cfg_attr(not(feature = "library"), entry_point)]
192pub fn execute(
193    deps: DepsMut,
194    env: Env,
195    info: MessageInfo,
196    msg: ExecuteMsg,
197) -> Result<Response, ContractError> {
198    match msg {
199        ExecuteMsg::Mint {} => execute_mint_sender(deps, env, info),
200        ExecuteMsg::Purge {} => execute_purge(deps, env, info),
201        ExecuteMsg::UpdateMintPrice { price } => execute_update_mint_price(deps, env, info, price),
202        ExecuteMsg::UpdateStartTime(time) => execute_update_start_time(deps, env, info, time),
203        ExecuteMsg::UpdateEndTime(time) => execute_update_end_time(deps, env, info, time),
204        ExecuteMsg::UpdateStartTradingTime(time) => {
205            execute_update_start_trading_time(deps, env, info, time)
206        }
207        ExecuteMsg::UpdatePerAddressLimit { per_address_limit } => {
208            execute_update_per_address_limit(deps, env, info, per_address_limit)
209        }
210        ExecuteMsg::MintTo { recipient } => execute_mint_to(deps, env, info, recipient),
211        ExecuteMsg::SetWhitelist { whitelist } => {
212            execute_set_whitelist(deps, env, info, &whitelist)
213        }
214        ExecuteMsg::BurnRemaining {} => execute_burn_remaining(deps, env, info),
215    }
216}
217
218// Purge frees data after a mint has ended
219// Anyone can purge
220pub fn execute_purge(
221    deps: DepsMut,
222    env: Env,
223    info: MessageInfo,
224) -> Result<Response, ContractError> {
225    nonpayable(&info)?;
226
227    // Check if mint has ended (optional)
228    let end_time = CONFIG.load(deps.storage)?.extension.end_time;
229    if let Some(end_time_u) = end_time {
230        if env.block.time <= end_time_u {
231            return Err(ContractError::MintingHasNotYetEnded {});
232        }
233    }
234
235    // check if sold out before end time (optional)
236    let mintable_num_tokens = MINTABLE_NUM_TOKENS.may_load(deps.storage)?;
237    if let Some(mintable_nb_tokens) = mintable_num_tokens {
238        if mintable_nb_tokens != 0 && end_time.is_none() {
239            return Err(ContractError::NotSoldOut {});
240        }
241    }
242
243    let keys = MINTER_ADDRS
244        .keys(deps.storage, None, None, Order::Ascending)
245        .collect::<Vec<_>>();
246    for key in keys {
247        MINTER_ADDRS.remove(deps.storage, &key?);
248    }
249
250    Ok(Response::new()
251        .add_attribute("action", "purge")
252        .add_attribute("contract", env.contract.address.to_string())
253        .add_attribute("sender", info.sender))
254}
255
256pub fn execute_set_whitelist(
257    deps: DepsMut,
258    env: Env,
259    info: MessageInfo,
260    whitelist: &str,
261) -> Result<Response, ContractError> {
262    nonpayable(&info)?;
263    let mut config = CONFIG.load(deps.storage)?;
264    let MinterConfig {
265        factory,
266        extension:
267            ConfigExtension {
268                whitelist: existing_whitelist,
269                admin,
270                start_time,
271                ..
272            },
273        ..
274    } = config.clone();
275    ensure!(
276        admin == info.sender,
277        ContractError::Unauthorized("Sender is not an admin".to_owned())
278    );
279
280    ensure!(
281        env.block.time < start_time,
282        ContractError::AlreadyStarted {}
283    );
284
285    if let Some(whitelist) = existing_whitelist {
286        let res: WhitelistConfigResponse = deps
287            .querier
288            .query_wasm_smart(whitelist, &WhitelistQueryMsg::Config {})?;
289
290        ensure!(!res.is_active, ContractError::WhitelistAlreadyStarted {});
291    }
292
293    let new_wl = deps.api.addr_validate(whitelist)?;
294    config.extension.whitelist = Some(new_wl.clone());
295    // check that the new whitelist exists
296    let WhitelistConfigResponse {
297        is_active: wl_is_active,
298        mint_price: wl_mint_price,
299        ..
300    } = deps
301        .querier
302        .query_wasm_smart(new_wl, &WhitelistQueryMsg::Config {})?;
303
304    ensure!(!wl_is_active, ContractError::WhitelistAlreadyStarted {});
305
306    ensure!(
307        wl_mint_price.denom == config.mint_price.denom,
308        ContractError::InvalidDenom {
309            expected: config.mint_price.denom,
310            got: wl_mint_price.denom,
311        }
312    );
313
314    // Whitelist could be free, while factory minimum is not
315    let ParamsResponse {
316        params:
317            OpenEditionMinterParams {
318                min_mint_price: factory_min_mint_price,
319                ..
320            },
321    } = deps
322        .querier
323        .query_wasm_smart(factory, &Sg2QueryMsg::Params {})?;
324
325    ensure!(
326        factory_min_mint_price.amount <= wl_mint_price.amount,
327        ContractError::InsufficientWhitelistMintPrice {
328            expected: factory_min_mint_price.amount.into(),
329            got: wl_mint_price.amount.into(),
330        }
331    );
332
333    // Whitelist denom should match factory mint denom
334    ensure!(
335        factory_min_mint_price.denom == wl_mint_price.denom,
336        ContractError::InvalidDenom {
337            expected: factory_min_mint_price.denom,
338            got: wl_mint_price.denom,
339        }
340    );
341
342    CONFIG.save(deps.storage, &config)?;
343
344    Ok(Response::default()
345        .add_attribute("action", "set_whitelist")
346        .add_attribute("whitelist", whitelist.to_string()))
347}
348
349pub fn execute_mint_sender(
350    deps: DepsMut,
351    env: Env,
352    info: MessageInfo,
353) -> Result<Response, ContractError> {
354    let config = CONFIG.load(deps.storage)?;
355    let action = "mint_sender";
356
357    // If there is no active whitelist right now, check public mint
358    // Check start and end time (if not optional)
359    if is_public_mint(deps.as_ref(), &info)? && (env.block.time < config.extension.start_time) {
360        return Err(ContractError::BeforeMintStartTime {});
361    }
362    if let Some(end_time) = config.extension.end_time {
363        if env.block.time >= end_time {
364            return Err(ContractError::AfterMintEndTime {});
365        }
366    }
367
368    // Check if already minted max per address limit
369    if matches!(mint_count_per_addr(deps.as_ref(), &info)?, count if count >= config.extension.per_address_limit)
370    {
371        return Err(ContractError::MaxPerAddressLimitExceeded {});
372    }
373
374    _execute_mint(deps, env, info, action, false, None)
375}
376
377// Check if a whitelist exists and not ended
378// Sender has to be whitelisted to mint
379fn is_public_mint(deps: Deps, info: &MessageInfo) -> Result<bool, ContractError> {
380    let config = CONFIG.load(deps.storage)?;
381
382    // If there is no whitelist, there's only a public mint
383    if config.extension.whitelist.is_none() {
384        return Ok(true);
385    }
386
387    let whitelist = config.extension.whitelist.unwrap();
388
389    let wl_config: WhitelistConfigResponse = deps
390        .querier
391        .query_wasm_smart(whitelist.clone(), &WhitelistQueryMsg::Config {})?;
392
393    if !wl_config.is_active {
394        return Ok(true);
395    }
396
397    let res: HasMemberResponse = deps.querier.query_wasm_smart(
398        whitelist,
399        &WhitelistQueryMsg::HasMember {
400            member: info.sender.to_string(),
401        },
402    )?;
403    if !res.has_member {
404        return Err(ContractError::NotWhitelisted {
405            addr: info.sender.to_string(),
406        });
407    }
408
409    // Check wl per address limit
410    let mint_count = mint_count(deps, info)?;
411    if mint_count >= wl_config.per_address_limit {
412        return Err(ContractError::MaxPerAddressLimitExceeded {});
413    }
414
415    Ok(false)
416}
417
418fn mint_count(deps: Deps, info: &MessageInfo) -> Result<u32, StdError> {
419    let mint_count = MINTER_ADDRS
420        .key(&info.sender)
421        .may_load(deps.storage)?
422        .unwrap_or(0);
423    Ok(mint_count)
424}
425
426pub fn execute_mint_to(
427    deps: DepsMut,
428    env: Env,
429    info: MessageInfo,
430    recipient: String,
431) -> Result<Response, ContractError> {
432    let recipient = deps.api.addr_validate(&recipient)?;
433    let config = CONFIG.load(deps.storage)?;
434    let action = "mint_to";
435
436    // Check only admin
437    if info.sender != config.extension.admin {
438        return Err(ContractError::Unauthorized(
439            "Sender is not an admin".to_owned(),
440        ));
441    }
442
443    if let Some(end_time) = config.extension.end_time {
444        if env.block.time >= end_time {
445            return Err(ContractError::AfterMintEndTime {});
446        }
447    }
448
449    _execute_mint(deps, env, info, action, true, Some(recipient))
450}
451
452// Generalize checks and mint message creation
453// mint -> _execute_mint(recipient: None, token_id: None)
454// mint_to(recipient: "friend") -> _execute_mint(Some(recipient), token_id: None)
455fn _execute_mint(
456    deps: DepsMut,
457    _env: Env,
458    info: MessageInfo,
459    action: &str,
460    is_admin: bool,
461    recipient: Option<Addr>,
462) -> Result<Response, ContractError> {
463    let mintable_num_tokens = MINTABLE_NUM_TOKENS.may_load(deps.storage)?;
464    if let Some(mintable_nb_tokens) = mintable_num_tokens {
465        if mintable_nb_tokens == 0 {
466            return Err(ContractError::SoldOut {});
467        }
468    }
469    let config = CONFIG.load(deps.storage)?;
470
471    let sg721_address = SG721_ADDRESS.load(deps.storage)?;
472
473    let recipient_addr = match recipient {
474        Some(some_recipient) => some_recipient,
475        None => info.sender.clone(),
476    };
477
478    let mint_price: Coin = mint_price(deps.as_ref(), is_admin)?;
479    // Exact payment only accepted
480    let payment = may_pay(&info, &mint_price.denom)?;
481    if payment != mint_price.amount {
482        return Err(ContractError::IncorrectPaymentAmount(
483            coin(payment.u128(), &config.mint_price.denom),
484            mint_price,
485        ));
486    }
487
488    let mut res = Response::new();
489
490    let factory: ParamsResponse = deps
491        .querier
492        .query_wasm_smart(config.factory, &Sg2QueryMsg::Params {})?;
493    let factory_params = factory.params;
494
495    // Create fee msgs
496    // Metadata Storage fees -> minting fee will be enabled for on-chain metadata mints
497    // dev fees are intrinsic in the mint fee (assuming a 50% share)
498    let mint_fee = if is_admin {
499        Decimal::bps(factory_params.extension.airdrop_mint_fee_bps)
500    } else {
501        Decimal::bps(factory_params.mint_fee_bps)
502    };
503    let network_fee = mint_price.amount * mint_fee;
504
505    if !network_fee.is_zero() {
506        distribute_mint_fees(
507            coin(network_fee.u128(), mint_price.clone().denom),
508            &mut res,
509            false,
510            Some(
511                deps.api
512                    .addr_validate(&factory_params.extension.dev_fee_address)?,
513            ),
514        )?;
515    }
516
517    // Token ID to mint + update the config counter
518    let token_id = increment_token_index(deps.storage)?.to_string();
519
520    // Create mint msg -> dependents on the NFT data type
521    let msg = mint_nft_msg(
522        sg721_address,
523        token_id.clone(),
524        recipient_addr.clone(),
525        match config.extension.nft_data.nft_data_type {
526            NftMetadataType::OnChainMetadata => config.extension.nft_data.extension,
527            NftMetadataType::OffChainMetadata => None,
528        },
529        match config.extension.nft_data.nft_data_type {
530            NftMetadataType::OnChainMetadata => None,
531            NftMetadataType::OffChainMetadata => config.extension.nft_data.token_uri,
532        },
533    )?;
534    res = res.add_message(msg);
535
536    // Save the new mint count for the sender's address
537    let new_mint_count = mint_count_per_addr(deps.as_ref(), &info)? + 1;
538    MINTER_ADDRS.save(deps.storage, &info.sender, &new_mint_count)?;
539
540    // Update the mint count
541    TOTAL_MINT_COUNT.update(
542        deps.storage,
543        |mut updated_mint_count| -> Result<_, ContractError> {
544            updated_mint_count += 1u32;
545            Ok(updated_mint_count)
546        },
547    )?;
548
549    // Update mintable count (optional)
550    if let Some(mintable_nb_tokens) = mintable_num_tokens {
551        MINTABLE_NUM_TOKENS.save(deps.storage, &(mintable_nb_tokens - 1))?;
552    }
553
554    let seller_amount = {
555        // the net amount is mint price - network fee (mint free + dev fee)
556        let amount = mint_price.amount.checked_sub(network_fee)?;
557        let payment_address = config.extension.payment_address;
558        let seller = config.extension.admin;
559        // Sending 0 coins fails, so only send if amount is non-zero
560        if !amount.is_zero() {
561            let msg = BankMsg::Send {
562                to_address: payment_address.unwrap_or(seller).to_string(),
563                amount: vec![coin(amount.u128(), mint_price.clone().denom)],
564            };
565            res = res.add_message(msg);
566        }
567        amount
568    };
569
570    Ok(res
571        .add_attribute("action", action)
572        .add_attribute("sender", info.sender)
573        .add_attribute("recipient", recipient_addr)
574        .add_attribute("token_id", token_id)
575        .add_attribute(
576            "network_fee",
577            coin(network_fee.into(), mint_price.clone().denom).to_string(),
578        )
579        .add_attribute("mint_price", mint_price.to_string())
580        .add_attribute(
581            "seller_amount",
582            coin(seller_amount.into(), mint_price.denom).to_string(),
583        ))
584}
585
586pub fn execute_update_mint_price(
587    deps: DepsMut,
588    env: Env,
589    info: MessageInfo,
590    price: u128,
591) -> Result<Response, ContractError> {
592    nonpayable(&info)?;
593    let mut config = CONFIG.load(deps.storage)?;
594    if info.sender != config.extension.admin {
595        return Err(ContractError::Unauthorized(
596            "Sender is not an admin".to_owned(),
597        ));
598    }
599
600    if let Some(end_time) = config.extension.end_time {
601        if env.block.time >= end_time {
602            return Err(ContractError::AfterMintEndTime {});
603        }
604    }
605
606    // If current time is after the stored start_time, only allow lowering price
607    if env.block.time >= config.extension.start_time && price >= config.mint_price.amount.u128() {
608        return Err(ContractError::UpdatedMintPriceTooHigh {
609            allowed: config.mint_price.amount.u128(),
610            updated: price,
611        });
612    }
613
614    let factory: ParamsResponse = deps
615        .querier
616        .query_wasm_smart(config.clone().factory, &Sg2QueryMsg::Params {})?;
617    let factory_params = factory.params;
618
619    if factory_params.min_mint_price.amount.u128() > price {
620        return Err(ContractError::InsufficientMintPrice {
621            expected: factory_params.min_mint_price.amount.u128(),
622            got: price,
623        });
624    }
625
626    if config.extension.num_tokens.is_none() {
627        ensure!(price != 0, ContractError::NoTokenLimitWithZeroMintPrice {})
628    }
629
630    config.mint_price = coin(price, config.mint_price.denom);
631    CONFIG.save(deps.storage, &config)?;
632    Ok(Response::new()
633        .add_attribute("action", "update_mint_price")
634        .add_attribute("sender", info.sender)
635        .add_attribute("mint_price", config.mint_price.to_string()))
636}
637
638pub fn execute_update_start_time(
639    deps: DepsMut,
640    env: Env,
641    info: MessageInfo,
642    start_time: Timestamp,
643) -> Result<Response, ContractError> {
644    nonpayable(&info)?;
645    let mut config = CONFIG.load(deps.storage)?;
646    if info.sender != config.extension.admin {
647        return Err(ContractError::Unauthorized(
648            "Sender is not an admin".to_owned(),
649        ));
650    }
651    // If current time is after the stored start time return error
652    if env.block.time >= config.extension.start_time {
653        return Err(ContractError::AlreadyStarted {});
654    }
655
656    // If current time already passed the new start_time return error
657    if env.block.time > start_time {
658        return Err(ContractError::InvalidStartTime(start_time, env.block.time));
659    }
660
661    // If the new start_time is after end_time return error
662    if let Some(end_time) = config.extension.end_time {
663        if start_time > end_time {
664            return Err(ContractError::InvalidStartTime(end_time, start_time));
665        }
666    }
667
668    config.extension.start_time = start_time;
669    CONFIG.save(deps.storage, &config)?;
670    Ok(Response::new()
671        .add_attribute("action", "update_start_time")
672        .add_attribute("sender", info.sender)
673        .add_attribute("start_time", start_time.to_string()))
674}
675
676pub fn execute_update_end_time(
677    deps: DepsMut,
678    env: Env,
679    info: MessageInfo,
680    end_time: Timestamp,
681) -> Result<Response, ContractError> {
682    nonpayable(&info)?;
683    let mut config = CONFIG.load(deps.storage)?;
684    if info.sender != config.extension.admin {
685        return Err(ContractError::Unauthorized(
686            "Sender is not an admin".to_owned(),
687        ));
688    }
689    // If current time is after the stored end time return error
690    if let Some(end_time_u) = config.extension.end_time {
691        if env.block.time >= end_time_u {
692            return Err(ContractError::AfterMintEndTime {});
693        }
694    } else {
695        // Cant define a end time if it was not initially defined to have one
696        return Err(ContractError::NoEndTimeInitiallyDefined {});
697    }
698
699    // If current time already passed the new end_time return error
700    if env.block.time > end_time {
701        return Err(ContractError::InvalidEndTime(end_time, env.block.time));
702    }
703
704    // If the new end_time if before the start_time return error
705    if end_time < config.extension.start_time {
706        return Err(ContractError::InvalidEndTime(
707            end_time,
708            config.extension.start_time,
709        ));
710    }
711
712    config.extension.end_time = Some(end_time);
713    CONFIG.save(deps.storage, &config)?;
714    Ok(Response::new()
715        .add_attribute("action", "update_end_time")
716        .add_attribute("sender", info.sender)
717        .add_attribute("end_time", end_time.to_string()))
718}
719
720pub fn execute_update_start_trading_time(
721    deps: DepsMut,
722    env: Env,
723    info: MessageInfo,
724    start_time: Option<Timestamp>,
725) -> Result<Response, ContractError> {
726    nonpayable(&info)?;
727    let config = CONFIG.load(deps.storage)?;
728    let sg721_contract_addr = SG721_ADDRESS.load(deps.storage)?;
729
730    if info.sender != config.extension.admin {
731        return Err(ContractError::Unauthorized(
732            "Sender is not an admin".to_owned(),
733        ));
734    }
735
736    // add custom rules here
737    let factory_params: ParamsResponse = deps
738        .querier
739        .query_wasm_smart(config.factory.clone(), &Sg2QueryMsg::Params {})?;
740    let default_start_time_with_offset = config
741        .extension
742        .start_time
743        .plus_seconds(factory_params.params.max_trading_offset_secs);
744
745    if let Some(start_trading_time) = start_time {
746        if env.block.time > start_trading_time {
747            return Err(ContractError::InvalidStartTradingTime(
748                env.block.time,
749                start_trading_time,
750            ));
751        }
752        // If new start_trading_time > old start time + offset , return error
753        if start_trading_time > default_start_time_with_offset {
754            return Err(ContractError::InvalidStartTradingTime(
755                start_trading_time,
756                default_start_time_with_offset,
757            ));
758        }
759    }
760
761    // execute sg721 contract
762    let msg = WasmMsg::Execute {
763        contract_addr: sg721_contract_addr.to_string(),
764        msg: to_json_binary(&Sg721ExecuteMsg::<Empty, Empty>::UpdateStartTradingTime(
765            start_time,
766        ))?,
767        funds: vec![],
768    };
769
770    Ok(Response::new()
771        .add_attribute("action", "update_start_trading_time")
772        .add_attribute("sender", info.sender)
773        .add_message(msg))
774}
775
776pub fn execute_update_per_address_limit(
777    deps: DepsMut,
778    _env: Env,
779    info: MessageInfo,
780    per_address_limit: u32,
781) -> Result<Response, ContractError> {
782    nonpayable(&info)?;
783    let mut config = CONFIG.load(deps.storage)?;
784    if info.sender != config.extension.admin {
785        return Err(ContractError::Unauthorized(
786            "Sender is not an admin".to_owned(),
787        ));
788    }
789
790    let factory: ParamsResponse = deps
791        .querier
792        .query_wasm_smart(config.factory.clone(), &Sg2QueryMsg::Params {})?;
793    let factory_params = factory.params;
794
795    if per_address_limit == 0 || per_address_limit > factory_params.extension.max_per_address_limit
796    {
797        return Err(ContractError::InvalidPerAddressLimit {
798            max: factory_params.extension.max_per_address_limit,
799            min: 1,
800            got: per_address_limit,
801        });
802    }
803
804    config.extension.per_address_limit = per_address_limit;
805    CONFIG.save(deps.storage, &config)?;
806    Ok(Response::new()
807        .add_attribute("action", "update_per_address_limit")
808        .add_attribute("sender", info.sender)
809        .add_attribute("limit", per_address_limit.to_string()))
810}
811
812// if admin_no_fee => no fee,
813// else if in whitelist => whitelist price
814// else => config unit price
815pub fn mint_price(deps: Deps, is_admin: bool) -> Result<Coin, StdError> {
816    let config = CONFIG.load(deps.storage)?;
817
818    if is_admin {
819        let factory: ParamsResponse = deps
820            .querier
821            .query_wasm_smart(config.factory, &Sg2QueryMsg::Params {})?;
822        let factory_params = factory.params;
823        if factory_params.extension.airdrop_mint_price.amount.is_zero() {
824            ensure!(
825                config.extension.num_tokens.is_some(),
826                StdError::generic_err(
827                    "Open Edition collections should have a non-zero airdrop price"
828                )
829            );
830        }
831        Ok(coin(
832            factory_params.extension.airdrop_mint_price.amount.u128(),
833            factory_params.extension.airdrop_mint_price.denom,
834        ))
835    } else {
836        if config.extension.whitelist.is_none() {
837            return Ok(config.mint_price.clone());
838        }
839        let whitelist = config.extension.whitelist.unwrap();
840        let whitelist_config: WhitelistConfigResponse = deps
841            .querier
842            .query_wasm_smart(whitelist, &WhitelistQueryMsg::Config {})?;
843
844        if whitelist_config.is_active {
845            Ok(whitelist_config.mint_price)
846        } else {
847            Ok(config.mint_price.clone())
848        }
849    }
850}
851
852pub fn execute_burn_remaining(
853    deps: DepsMut,
854    env: Env,
855    info: MessageInfo,
856) -> Result<Response, ContractError> {
857    nonpayable(&info)?;
858    let config = CONFIG.load(deps.storage)?;
859    // Check only admin
860    if info.sender != config.extension.admin {
861        return Err(ContractError::Unauthorized(
862            "Sender is not an admin".to_owned(),
863        ));
864    }
865
866    // check mint if still time to mint
867    if let Some(end_time) = config.extension.end_time {
868        if env.block.time <= end_time {
869            return Err(ContractError::MintingHasNotYetEnded {});
870        }
871    }
872
873    // check mint not sold out
874    let mintable_num_tokens = MINTABLE_NUM_TOKENS.may_load(deps.storage)?;
875    if let Some(mintable_nb_tokens) = mintable_num_tokens {
876        if mintable_nb_tokens == 0 {
877            return Err(ContractError::SoldOut {});
878        }
879    }
880
881    // Decrement mintable num tokens
882    if mintable_num_tokens.is_some() {
883        MINTABLE_NUM_TOKENS.save(deps.storage, &0)?;
884    }
885
886    let event = Event::new("burn-remaining")
887        .add_attribute("sender", info.sender)
888        .add_attribute("tokens_burned", mintable_num_tokens.unwrap().to_string())
889        .add_attribute("minter", env.contract.address.to_string());
890    Ok(Response::new().add_event(event))
891}
892
893fn mint_count_per_addr(deps: Deps, info: &MessageInfo) -> Result<u32, StdError> {
894    let mint_count = (MINTER_ADDRS.key(&info.sender).may_load(deps.storage)?).unwrap_or(0);
895    Ok(mint_count)
896}
897
898#[cfg_attr(not(feature = "library"), entry_point)]
899pub fn sudo(deps: DepsMut, _env: Env, msg: SudoMsg) -> Result<Response, ContractError> {
900    match msg {
901        SudoMsg::UpdateStatus {
902            is_verified,
903            is_blocked,
904            is_explicit,
905        } => update_status(deps, is_verified, is_blocked, is_explicit)
906            .map_err(|_| ContractError::UpdateStatus {}),
907    }
908}
909
910/// Only governance can update contract params
911pub fn update_status(
912    deps: DepsMut,
913    is_verified: bool,
914    is_blocked: bool,
915    is_explicit: bool,
916) -> StdResult<Response> {
917    let mut status = STATUS.load(deps.storage)?;
918    status.is_verified = is_verified;
919    status.is_blocked = is_blocked;
920    status.is_explicit = is_explicit;
921
922    Ok(Response::new().add_attribute("action", "sudo_update_status"))
923}
924
925#[cfg_attr(not(feature = "library"), entry_point)]
926pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
927    match msg {
928        QueryMsg::Config {} => to_json_binary(&query_config(deps)?),
929        QueryMsg::Status {} => to_json_binary(&query_status(deps)?),
930        QueryMsg::StartTime {} => to_json_binary(&query_start_time(deps)?),
931        QueryMsg::EndTime {} => to_json_binary(&query_end_time(deps)?),
932        QueryMsg::MintPrice {} => to_json_binary(&query_mint_price(deps)?),
933        QueryMsg::MintCount { address } => {
934            to_json_binary(&query_mint_count_per_address(deps, address)?)
935        }
936        QueryMsg::TotalMintCount {} => to_json_binary(&query_mint_count(deps)?),
937        QueryMsg::MintableNumTokens {} => to_json_binary(&query_mintable_num_tokens(deps)?),
938    }
939}
940
941fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
942    let config = CONFIG.load(deps.storage)?;
943    let sg721_address = SG721_ADDRESS.load(deps.storage)?;
944
945    Ok(ConfigResponse {
946        admin: config.extension.admin.to_string(),
947        nft_data: config.extension.nft_data,
948        payment_address: config.extension.payment_address,
949        per_address_limit: config.extension.per_address_limit,
950        num_tokens: config.extension.num_tokens,
951        end_time: config.extension.end_time,
952        sg721_address: sg721_address.to_string(),
953        sg721_code_id: config.collection_code_id,
954        start_time: config.extension.start_time,
955        mint_price: config.mint_price,
956        factory: config.factory.to_string(),
957        whitelist: config.extension.whitelist.map(|w| w.to_string()),
958    })
959}
960
961pub fn query_status(deps: Deps) -> StdResult<StatusResponse> {
962    let status = STATUS.load(deps.storage)?;
963
964    Ok(StatusResponse { status })
965}
966
967fn query_mint_count_per_address(deps: Deps, address: String) -> StdResult<MintCountResponse> {
968    let addr = deps.api.addr_validate(&address)?;
969    let mint_count = (MINTER_ADDRS.key(&addr).may_load(deps.storage)?).unwrap_or(0);
970    Ok(MintCountResponse {
971        address: addr.to_string(),
972        count: mint_count,
973    })
974}
975
976fn query_mint_count(deps: Deps) -> StdResult<TotalMintCountResponse> {
977    let mint_count = TOTAL_MINT_COUNT.load(deps.storage)?;
978    Ok(TotalMintCountResponse { count: mint_count })
979}
980
981fn query_mintable_num_tokens(deps: Deps) -> StdResult<MintableNumTokensResponse> {
982    let count = MINTABLE_NUM_TOKENS.may_load(deps.storage)?;
983    Ok(MintableNumTokensResponse { count })
984}
985
986fn query_start_time(deps: Deps) -> StdResult<StartTimeResponse> {
987    let config = CONFIG.load(deps.storage)?;
988    Ok(StartTimeResponse {
989        start_time: config.extension.start_time.to_string(),
990    })
991}
992
993fn query_end_time(deps: Deps) -> StdResult<EndTimeResponse> {
994    let config = CONFIG.load(deps.storage)?;
995    let end_time_response = config
996        .extension
997        .end_time
998        .map(|end_time| EndTimeResponse {
999            end_time: Some(end_time.to_string()),
1000        })
1001        .unwrap_or(EndTimeResponse { end_time: None });
1002
1003    Ok(end_time_response)
1004}
1005
1006fn query_mint_price(deps: Deps) -> StdResult<MintPriceResponse> {
1007    let config = CONFIG.load(deps.storage)?;
1008
1009    let factory: ParamsResponse = deps
1010        .querier
1011        .query_wasm_smart(config.factory, &Sg2QueryMsg::Params {})?;
1012
1013    let factory_params = factory.params;
1014
1015    let current_price = mint_price(deps, false)?;
1016    let public_price = config.mint_price.clone();
1017    let whitelist_price: Option<Coin> = if let Some(whitelist) = config.extension.whitelist {
1018        let wl_config: WhitelistConfigResponse = deps
1019            .querier
1020            .query_wasm_smart(whitelist, &WhitelistQueryMsg::Config {})?;
1021        Some(wl_config.mint_price)
1022    } else {
1023        None
1024    };
1025    let airdrop_price = coin(
1026        factory_params.extension.airdrop_mint_price.amount.u128(),
1027        config.mint_price.denom,
1028    );
1029    Ok(MintPriceResponse {
1030        public_price,
1031        airdrop_price,
1032        whitelist_price,
1033        current_price,
1034    })
1035}
1036
1037// Reply callback triggered from cw721 contract instantiation
1038#[cfg_attr(not(feature = "library"), entry_point)]
1039pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
1040    if msg.id != INSTANTIATE_SG721_REPLY_ID {
1041        return Err(ContractError::InvalidReplyID {});
1042    }
1043
1044    let reply = parse_reply_instantiate_data(msg);
1045    match reply {
1046        Ok(res) => {
1047            let sg721_address = res.contract_address;
1048            SG721_ADDRESS.save(deps.storage, &Addr::unchecked(sg721_address.clone()))?;
1049            Ok(Response::default()
1050                .add_attribute("action", "instantiate_sg721_reply")
1051                .add_attribute("sg721_address", sg721_address))
1052        }
1053        Err(_) => Err(ContractError::InstantiateSg721Error {}),
1054    }
1055}
1056
1057#[cfg_attr(not(feature = "library"), entry_point)]
1058pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result<Response, ContractError> {
1059    let current_version = cw2::get_contract_version(deps.storage)?;
1060    if current_version.contract != CONTRACT_NAME {
1061        return Err(StdError::generic_err("Cannot upgrade to a different contract").into());
1062    }
1063    let version: Version = current_version
1064        .version
1065        .parse()
1066        .map_err(|_| StdError::generic_err("Invalid contract version"))?;
1067    let new_version: Version = CONTRACT_VERSION
1068        .parse()
1069        .map_err(|_| StdError::generic_err("Invalid contract version"))?;
1070
1071    if version > new_version {
1072        return Err(StdError::generic_err("Cannot upgrade to a previous contract version").into());
1073    }
1074    // if same version return
1075    if version == new_version {
1076        return Ok(Response::new());
1077    }
1078
1079    // set new contract version
1080    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
1081    Ok(Response::new())
1082}