#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
coin, to_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Empty, Env,
MessageInfo, Order, Reply, ReplyOn, StdError, StdResult, Timestamp, Uint128, WasmMsg,
};
use cw2::set_contract_version;
use cw721_base::{msg::ExecuteMsg as Cw721ExecuteMsg, MintMsg};
use cw_utils::{may_pay, parse_reply_instantiate_data};
use sg1::checked_fair_burn;
use sg721::msg::InstantiateMsg as Sg721InstantiateMsg;
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::{StargazeMsgWrapper, GENESIS_MINT_START_TIME, NATIVE_DENOM};
use sg_whitelist::msg::{
ConfigResponse as WhitelistConfigResponse, HasMemberResponse, QueryMsg as WhitelistQueryMsg,
};
pub type Response = cosmwasm_std::Response<StargazeMsgWrapper>;
pub type SubMsg = cosmwasm_std::SubMsg<StargazeMsgWrapper>;
const CONTRACT_NAME: &str = "crates.io:sg-minter";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
const INSTANTIATE_SG721_REPLY_ID: u64 = 1;
const MAX_TOKEN_LIMIT: u32 = 10000;
const MAX_PER_ADDRESS_LIMIT: u32 = 50;
const MIN_MINT_PRICE: u128 = 50_000_000;
const AIRDROP_MINT_PRICE: u128 = 15_000_000;
const MINT_FEE_PERCENT: u32 = 10;
const AIRDROP_MINT_FEE_PERCENT: u32 = 100;
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
if msg.num_tokens == 0 || msg.num_tokens > MAX_TOKEN_LIMIT {
return Err(ContractError::InvalidNumTokens {
min: 1,
max: MAX_TOKEN_LIMIT,
});
}
if msg.per_address_limit == 0 || msg.per_address_limit > MAX_PER_ADDRESS_LIMIT {
return Err(ContractError::InvalidPerAddressLimit {
max: MAX_PER_ADDRESS_LIMIT,
min: 1,
got: msg.per_address_limit,
});
}
let parsed_token_uri = Url::parse(&msg.base_token_uri)?;
if parsed_token_uri.scheme() != "ipfs" {
return Err(ContractError::InvalidBaseTokenURI {});
}
if NATIVE_DENOM != msg.unit_price.denom {
return Err(ContractError::InvalidDenom {
expected: NATIVE_DENOM.to_string(),
got: msg.unit_price.denom,
});
}
if MIN_MINT_PRICE > msg.unit_price.amount.into() {
return Err(ContractError::InsufficientMintPrice {
expected: MIN_MINT_PRICE,
got: msg.unit_price.amount.into(),
});
}
let genesis_time = Timestamp::from_nanos(GENESIS_MINT_START_TIME);
if msg.start_time < genesis_time {
return Err(ContractError::BeforeGenesisTime {});
}
if env.block.time > msg.start_time {
return Err(ContractError::InvalidStartTime(
msg.start_time,
env.block.time,
));
}
let whitelist_addr = msg
.whitelist
.and_then(|w| deps.api.addr_validate(w.as_str()).ok());
let config = Config {
admin: info.sender.clone(),
base_token_uri: msg.base_token_uri,
num_tokens: msg.num_tokens,
sg721_code_id: msg.sg721_code_id,
unit_price: msg.unit_price,
per_address_limit: msg.per_address_limit,
whitelist: whitelist_addr,
start_time: msg.start_time,
};
CONFIG.save(deps.storage, &config)?;
MINTABLE_NUM_TOKENS.save(deps.storage, &msg.num_tokens)?;
for token_id in 1..=msg.num_tokens {
MINTABLE_TOKEN_IDS.save(deps.storage, token_id, &true)?;
}
let sub_msgs: Vec<SubMsg> = vec![SubMsg {
msg: WasmMsg::Instantiate {
code_id: msg.sg721_code_id,
msg: to_binary(&Sg721InstantiateMsg {
name: msg.sg721_instantiate_msg.name,
symbol: msg.sg721_instantiate_msg.symbol,
minter: env.contract.address.to_string(),
collection_info: msg.sg721_instantiate_msg.collection_info,
})?,
funds: info.funds,
admin: Some(info.sender.to_string()),
label: String::from("Fixed price minter"),
}
.into(),
id: INSTANTIATE_SG721_REPLY_ID,
gas_limit: None,
reply_on: ReplyOn::Success,
}];
Ok(Response::new()
.add_attribute("action", "instantiate")
.add_attribute("contract_name", CONTRACT_NAME)
.add_attribute("contract_version", CONTRACT_VERSION)
.add_attribute("sender", info.sender)
.add_submessages(sub_msgs))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Mint {} => execute_mint_sender(deps, env, info),
ExecuteMsg::UpdateStartTime(time) => execute_update_start_time(deps, env, info, time),
ExecuteMsg::UpdatePerAddressLimit { per_address_limit } => {
execute_update_per_address_limit(deps, env, info, per_address_limit)
}
ExecuteMsg::MintTo { recipient } => execute_mint_to(deps, info, recipient),
ExecuteMsg::MintFor {
token_id,
recipient,
} => execute_mint_for(deps, info, token_id, recipient),
ExecuteMsg::SetWhitelist { whitelist } => {
execute_set_whitelist(deps, env, info, &whitelist)
}
}
}
pub fn execute_set_whitelist(
deps: DepsMut,
env: Env,
info: MessageInfo,
whitelist: &str,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if config.admin != info.sender {
return Err(ContractError::Unauthorized(
"Sender is not an admin".to_owned(),
));
};
if env.block.time >= config.start_time {
return Err(ContractError::AlreadyStarted {});
}
if let Some(wl) = config.whitelist {
let res: WhitelistConfigResponse = deps
.querier
.query_wasm_smart(wl, &WhitelistQueryMsg::Config {})?;
if res.is_active {
return Err(ContractError::WhitelistAlreadyStarted {});
}
}
config.whitelist = Some(deps.api.addr_validate(whitelist)?);
CONFIG.save(deps.storage, &config)?;
Ok(Response::default()
.add_attribute("action", "set_whitelist")
.add_attribute("whitelist", whitelist.to_string()))
}
pub fn execute_mint_sender(
deps: DepsMut,
env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let action = "mint_sender";
if is_public_mint(deps.as_ref(), &info)? && (env.block.time < config.start_time) {
return Err(ContractError::BeforeMintStartTime {});
}
let mint_count = mint_count(deps.as_ref(), &info)?;
if mint_count >= config.per_address_limit {
return Err(ContractError::MaxPerAddressLimitExceeded {});
}
_execute_mint(deps, info, action, false, None, None)
}
fn is_public_mint(deps: Deps, info: &MessageInfo) -> Result<bool, ContractError> {
let config = CONFIG.load(deps.storage)?;
if config.whitelist.is_none() {
return Ok(true);
}
let whitelist = config.whitelist.unwrap();
let wl_config: WhitelistConfigResponse = deps
.querier
.query_wasm_smart(whitelist.clone(), &WhitelistQueryMsg::Config {})?;
if !wl_config.is_active {
return Ok(true);
}
let res: HasMemberResponse = deps.querier.query_wasm_smart(
whitelist,
&WhitelistQueryMsg::HasMember {
member: info.sender.to_string(),
},
)?;
if !res.has_member {
return Err(ContractError::NotWhitelisted {
addr: info.sender.to_string(),
});
}
let mint_count = mint_count(deps, info)?;
if mint_count >= wl_config.per_address_limit {
return Err(ContractError::MaxPerAddressLimitExceeded {});
}
Ok(false)
}
pub fn execute_mint_to(
deps: DepsMut,
info: MessageInfo,
recipient: String,
) -> Result<Response, ContractError> {
let recipient = deps.api.addr_validate(&recipient)?;
let config = CONFIG.load(deps.storage)?;
let action = "mint_to";
if info.sender != config.admin {
return Err(ContractError::Unauthorized(
"Sender is not an admin".to_owned(),
));
}
_execute_mint(deps, info, action, true, Some(recipient), None)
}
pub fn execute_mint_for(
deps: DepsMut,
info: MessageInfo,
token_id: u32,
recipient: String,
) -> Result<Response, ContractError> {
let recipient = deps.api.addr_validate(&recipient)?;
let config = CONFIG.load(deps.storage)?;
let action = "mint_for";
if info.sender != config.admin {
return Err(ContractError::Unauthorized(
"Sender is not an admin".to_owned(),
));
}
_execute_mint(deps, info, action, true, Some(recipient), Some(token_id))
}
fn _execute_mint(
deps: DepsMut,
info: MessageInfo,
action: &str,
is_admin: bool,
recipient: Option<Addr>,
token_id: Option<u32>,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
let sg721_address = SG721_ADDRESS.load(deps.storage)?;
let recipient_addr = match recipient {
Some(some_recipient) => some_recipient,
None => info.sender.clone(),
};
let mint_price: Coin = mint_price(deps.as_ref(), is_admin)?;
let payment = may_pay(&info, &config.unit_price.denom)?;
if payment != mint_price.amount {
return Err(ContractError::IncorrectPaymentAmount(
coin(payment.u128(), &config.unit_price.denom),
mint_price,
));
}
let mut res = Response::new();
let fee_percent = if is_admin {
Decimal::percent(AIRDROP_MINT_FEE_PERCENT as u64)
} else {
Decimal::percent(MINT_FEE_PERCENT as u64)
};
let network_fee = mint_price.amount * fee_percent;
checked_fair_burn(&info, network_fee.u128(), None, &mut res)?;
let mintable_token_id = match token_id {
Some(token_id) => {
if token_id == 0 || token_id > config.num_tokens {
return Err(ContractError::InvalidTokenId {});
}
if !MINTABLE_TOKEN_IDS.has(deps.storage, token_id) {
return Err(ContractError::TokenIdAlreadySold { token_id });
}
token_id
}
None => {
let mintable_tokens_result: StdResult<Vec<u32>> = MINTABLE_TOKEN_IDS
.keys(deps.storage, None, None, Order::Ascending)
.take(1)
.collect();
let mintable_tokens = mintable_tokens_result?;
if mintable_tokens.is_empty() {
return Err(ContractError::SoldOut {});
}
mintable_tokens[0]
}
};
let mint_msg = Cw721ExecuteMsg::Mint(MintMsg::<Empty> {
token_id: mintable_token_id.to_string(),
owner: recipient_addr.to_string(),
token_uri: Some(format!("{}/{}", config.base_token_uri, mintable_token_id)),
extension: Empty {},
});
let msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: sg721_address.to_string(),
msg: to_binary(&mint_msg)?,
funds: vec![],
});
res = res.add_message(msg);
MINTABLE_TOKEN_IDS.remove(deps.storage, mintable_token_id);
let mintable_num_tokens = MINTABLE_NUM_TOKENS.load(deps.storage)?;
MINTABLE_NUM_TOKENS.save(deps.storage, &(mintable_num_tokens - 1))?;
let new_mint_count = mint_count(deps.as_ref(), &info)? + 1;
MINTER_ADDRS.save(deps.storage, info.clone().sender, &new_mint_count)?;
let seller_amount = if !is_admin {
let amount = mint_price.amount - network_fee;
let msg = BankMsg::Send {
to_address: config.admin.to_string(),
amount: vec![coin(amount.u128(), config.unit_price.denom)],
};
res = res.add_message(msg);
amount
} else {
Uint128::zero()
};
Ok(res
.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("network_fee", network_fee)
.add_attribute("mint_price", mint_price.amount)
.add_attribute("seller_amount", seller_amount))
}
pub fn execute_update_start_time(
deps: DepsMut,
env: Env,
info: MessageInfo,
start_time: Timestamp,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.admin {
return Err(ContractError::Unauthorized(
"Sender is not an admin".to_owned(),
));
}
if env.block.time >= config.start_time {
return Err(ContractError::AlreadyStarted {});
}
if env.block.time > start_time {
return Err(ContractError::InvalidStartTime(start_time, env.block.time));
}
let genesis_start_time = Timestamp::from_nanos(GENESIS_MINT_START_TIME);
if start_time < genesis_start_time {
return Err(ContractError::BeforeGenesisTime {});
}
config.start_time = start_time;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("action", "update_start_time")
.add_attribute("sender", info.sender)
.add_attribute("start_time", start_time.to_string()))
}
pub fn execute_update_per_address_limit(
deps: DepsMut,
_env: Env,
info: MessageInfo,
per_address_limit: u32,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.admin {
return Err(ContractError::Unauthorized(
"Sender is not an admin".to_owned(),
));
}
if per_address_limit == 0 || per_address_limit > MAX_PER_ADDRESS_LIMIT {
return Err(ContractError::InvalidPerAddressLimit {
max: MAX_PER_ADDRESS_LIMIT,
min: 1,
got: per_address_limit,
});
}
config.per_address_limit = per_address_limit;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("action", "update_per_address_limit")
.add_attribute("sender", info.sender)
.add_attribute("limit", per_address_limit.to_string()))
}
pub fn mint_price(deps: Deps, is_admin: bool) -> Result<Coin, StdError> {
let config = CONFIG.load(deps.storage)?;
if is_admin {
return Ok(coin(AIRDROP_MINT_PRICE, config.unit_price.denom));
}
if config.whitelist.is_none() {
return Ok(config.unit_price);
}
let whitelist = config.whitelist.unwrap();
let wl_config: WhitelistConfigResponse = deps
.querier
.query_wasm_smart(whitelist, &WhitelistQueryMsg::Config {})?;
if wl_config.is_active {
Ok(wl_config.unit_price)
} else {
Ok(config.unit_price)
}
}
fn mint_count(deps: Deps, info: &MessageInfo) -> Result<u32, StdError> {
let mint_count = (MINTER_ADDRS
.key(info.sender.clone())
.may_load(deps.storage)?)
.unwrap_or(0);
Ok(mint_count)
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Config {} => to_binary(&query_config(deps)?),
QueryMsg::StartTime {} => to_binary(&query_start_time(deps)?),
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)?),
}
}
fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
let config = CONFIG.load(deps.storage)?;
let sg721_address = SG721_ADDRESS.load(deps.storage)?;
Ok(ConfigResponse {
admin: config.admin.to_string(),
base_token_uri: config.base_token_uri,
sg721_address: sg721_address.to_string(),
sg721_code_id: config.sg721_code_id,
num_tokens: config.num_tokens,
start_time: config.start_time,
unit_price: config.unit_price,
per_address_limit: config.per_address_limit,
whitelist: config.whitelist.map(|w| w.to_string()),
})
}
fn query_mint_count(deps: Deps, address: String) -> StdResult<MintCountResponse> {
let addr = deps.api.addr_validate(&address)?;
let mint_count = (MINTER_ADDRS.key(addr.clone()).may_load(deps.storage)?).unwrap_or(0);
Ok(MintCountResponse {
address: addr.to_string(),
count: mint_count,
})
}
fn query_start_time(deps: Deps) -> StdResult<StartTimeResponse> {
let config = CONFIG.load(deps.storage)?;
Ok(StartTimeResponse {
start_time: config.start_time.to_string(),
})
}
fn query_mintable_num_tokens(deps: Deps) -> StdResult<MintableNumTokensResponse> {
let count = MINTABLE_NUM_TOKENS.load(deps.storage)?;
Ok(MintableNumTokensResponse { count })
}
fn query_mint_price(deps: Deps) -> StdResult<MintPriceResponse> {
let config = CONFIG.load(deps.storage)?;
let current_price = mint_price(deps, false)?;
let public_price = config.unit_price;
let whitelist_price: Option<Coin> = if let Some(whitelist) = config.whitelist {
let wl_config: WhitelistConfigResponse = deps
.querier
.query_wasm_smart(whitelist, &WhitelistQueryMsg::Config {})?;
Some(wl_config.unit_price)
} else {
None
};
Ok(MintPriceResponse {
current_price,
public_price,
whitelist_price,
})
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
if msg.id != INSTANTIATE_SG721_REPLY_ID {
return Err(ContractError::InvalidReplyID {});
}
let reply = parse_reply_instantiate_data(msg);
match reply {
Ok(res) => {
SG721_ADDRESS.save(deps.storage, &Addr::unchecked(res.contract_address))?;
Ok(Response::default().add_attribute("action", "instantiate_sg721_reply"))
}
Err(_) => Err(ContractError::InstantiateSg721Error {}),
}
}