base_minter/
contract.rs

1use crate::error::ContractError;
2use crate::msg::{ConfigResponse, ExecuteMsg};
3use crate::state::{increment_token_index, Config, COLLECTION_ADDRESS, CONFIG, STATUS};
4use base_factory::msg::{BaseMinterCreateMsg, ParamsResponse};
5use base_factory::state::Extension;
6#[cfg(not(feature = "library"))]
7use cosmwasm_std::entry_point;
8use cosmwasm_std::{
9    to_json_binary, Addr, Binary, CosmosMsg, Decimal, Deps, DepsMut, Empty, Env, MessageInfo,
10    Reply, StdResult, Timestamp, WasmMsg,
11};
12use cw2::set_contract_version;
13use cw_utils::{must_pay, nonpayable, parse_reply_instantiate_data};
14use sg1::checked_fair_burn;
15use sg2::query::Sg2QueryMsg;
16use sg4::{QueryMsg, Status, StatusResponse, SudoMsg};
17use sg721::{ExecuteMsg as Sg721ExecuteMsg, InstantiateMsg as Sg721InstantiateMsg};
18use sg721_base::msg::{CollectionInfoResponse, QueryMsg as Sg721QueryMsg};
19use sg_std::{Response, SubMsg, NATIVE_DENOM};
20use url::Url;
21
22const CONTRACT_NAME: &str = "crates.io:sg-base-minter";
23const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
24const INSTANTIATE_SG721_REPLY_ID: u64 = 1;
25
26#[cfg_attr(not(feature = "library"), entry_point)]
27pub fn instantiate(
28    deps: DepsMut,
29    env: Env,
30    info: MessageInfo,
31    msg: BaseMinterCreateMsg,
32) -> Result<Response, ContractError> {
33    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
34
35    let factory = info.sender.clone();
36
37    // set default status so it can be queried without failing
38    STATUS.save(deps.storage, &Status::default())?;
39
40    // Make sure the sender is the factory contract
41    // This will fail if the sender cannot parse a response from the factory contract
42    let factory_params: ParamsResponse = deps
43        .querier
44        .query_wasm_smart(factory.clone(), &Sg2QueryMsg::Params {})?;
45
46    let config = Config {
47        factory: factory.clone(),
48        collection_code_id: msg.collection_params.code_id,
49        // assume the mint price is the minimum mint price
50        // 100% is fair burned
51        mint_price: factory_params.params.min_mint_price,
52        extension: Empty {},
53    };
54
55    // Use default start trading time if not provided
56    let mut collection_info = msg.collection_params.info.clone();
57    let offset = factory_params.params.max_trading_offset_secs;
58    let start_trading_time = msg
59        .collection_params
60        .info
61        .start_trading_time
62        .or_else(|| Some(env.block.time.plus_seconds(offset)));
63    collection_info.start_trading_time = start_trading_time;
64
65    CONFIG.save(deps.storage, &config)?;
66
67    let wasm_msg = WasmMsg::Instantiate {
68        code_id: msg.collection_params.code_id,
69        msg: to_json_binary(&Sg721InstantiateMsg {
70            name: msg.collection_params.name.clone(),
71            symbol: msg.collection_params.symbol,
72            minter: env.contract.address.to_string(),
73            collection_info,
74        })?,
75        funds: info.funds,
76        admin: Some(
77            deps.api
78                .addr_validate(&msg.collection_params.info.creator)?
79                .to_string(),
80        ),
81        label: format!(
82            "SG721-{}-{}",
83            msg.collection_params.code_id,
84            msg.collection_params.name.trim()
85        ),
86    };
87    let submsg = SubMsg::reply_on_success(wasm_msg, INSTANTIATE_SG721_REPLY_ID);
88
89    Ok(Response::new()
90        .add_attribute("action", "instantiate")
91        .add_attribute("contract_name", CONTRACT_NAME)
92        .add_attribute("contract_version", CONTRACT_VERSION)
93        .add_attribute("sender", factory)
94        .add_submessage(submsg))
95}
96
97#[cfg_attr(not(feature = "library"), entry_point)]
98pub fn execute(
99    deps: DepsMut,
100    env: Env,
101    info: MessageInfo,
102    msg: ExecuteMsg,
103) -> Result<Response, ContractError> {
104    match msg {
105        ExecuteMsg::Mint { token_uri } => execute_mint_sender(deps, info, token_uri),
106        ExecuteMsg::UpdateStartTradingTime(time) => {
107            execute_update_start_trading_time(deps, env, info, time)
108        }
109    }
110}
111
112pub fn execute_mint_sender(
113    deps: DepsMut,
114    info: MessageInfo,
115    token_uri: String,
116) -> Result<Response, ContractError> {
117    let config = CONFIG.load(deps.storage)?;
118    let collection_address = COLLECTION_ADDRESS.load(deps.storage)?;
119
120    // This is a 1:1 minter, minted at min_mint_price
121    // Should mint and then list on the marketplace for secondary sales
122    let collection_info: CollectionInfoResponse = deps.querier.query_wasm_smart(
123        collection_address.clone(),
124        &Sg721QueryMsg::CollectionInfo {},
125    )?;
126    // allow only sg721 creator address to mint
127    if collection_info.creator != info.sender {
128        return Err(ContractError::Unauthorized(
129            "Sender is not sg721 creator".to_owned(),
130        ));
131    };
132
133    // Token URI must be a valid URL (ipfs, https, etc.)
134    Url::parse(&token_uri).map_err(|_| ContractError::InvalidTokenURI {})?;
135
136    let mut res = Response::new();
137
138    let factory: ParamsResponse = deps
139        .querier
140        .query_wasm_smart(config.factory, &Sg2QueryMsg::Params {})?;
141    let factory_params = factory.params;
142
143    let funds_sent = must_pay(&info, NATIVE_DENOM)?;
144
145    // Create network fee msgs
146    let mint_fee_percent = Decimal::bps(factory_params.mint_fee_bps);
147    let network_fee = config.mint_price.amount * mint_fee_percent;
148    // For the base 1/1 minter, the entire mint price should be Fair Burned
149    if network_fee != funds_sent {
150        return Err(ContractError::InvalidMintPrice {});
151    }
152    checked_fair_burn(&info, network_fee.u128(), None, &mut res)?;
153
154    // Create mint msgs
155    let mint_msg = Sg721ExecuteMsg::<Extension, Empty>::Mint {
156        token_id: increment_token_index(deps.storage)?.to_string(),
157        owner: info.sender.to_string(),
158        token_uri: Some(token_uri.clone()),
159        extension: None,
160    };
161    let msg = CosmosMsg::Wasm(WasmMsg::Execute {
162        contract_addr: collection_address.to_string(),
163        msg: to_json_binary(&mint_msg)?,
164        funds: vec![],
165    });
166    res = res.add_message(msg);
167
168    Ok(res
169        .add_attribute("action", "mint")
170        .add_attribute("sender", info.sender)
171        .add_attribute("token_uri", token_uri)
172        .add_attribute("network_fee", network_fee.to_string()))
173}
174
175pub fn execute_update_start_trading_time(
176    deps: DepsMut,
177    env: Env,
178    info: MessageInfo,
179    start_time: Option<Timestamp>,
180) -> Result<Response, ContractError> {
181    nonpayable(&info)?;
182    let sg721_contract_addr = COLLECTION_ADDRESS.load(deps.storage)?;
183
184    let collection_info: CollectionInfoResponse = deps.querier.query_wasm_smart(
185        sg721_contract_addr.clone(),
186        &Sg721QueryMsg::CollectionInfo {},
187    )?;
188    if info.sender != collection_info.creator {
189        return Err(ContractError::Unauthorized(
190            "Sender is not creator".to_owned(),
191        ));
192    }
193
194    // add custom rules here
195    if let Some(start_time) = start_time {
196        if env.block.time > start_time {
197            return Err(ContractError::InvalidStartTradingTime(
198                env.block.time,
199                start_time,
200            ));
201        }
202    }
203
204    // execute sg721 contract
205    let msg = WasmMsg::Execute {
206        contract_addr: sg721_contract_addr.to_string(),
207        msg: to_json_binary(
208            &Sg721ExecuteMsg::<Extension, Empty>::UpdateStartTradingTime(start_time),
209        )?,
210        funds: vec![],
211    };
212
213    Ok(Response::new()
214        .add_attribute("action", "update_start_time")
215        .add_attribute("sender", info.sender)
216        .add_message(msg))
217}
218
219#[cfg_attr(not(feature = "library"), entry_point)]
220pub fn sudo(deps: DepsMut, _env: Env, msg: SudoMsg) -> Result<Response, ContractError> {
221    match msg {
222        SudoMsg::UpdateStatus {
223            is_verified,
224            is_blocked,
225            is_explicit,
226        } => update_status(deps, is_verified, is_blocked, is_explicit)
227            .map_err(|_| ContractError::UpdateStatus {}),
228    }
229}
230
231/// Only governance can update contract params
232pub fn update_status(
233    deps: DepsMut,
234    is_verified: bool,
235    is_blocked: bool,
236    is_explicit: bool,
237) -> StdResult<Response> {
238    let mut status = STATUS.load(deps.storage)?;
239    status.is_verified = is_verified;
240    status.is_blocked = is_blocked;
241    status.is_explicit = is_explicit;
242    STATUS.save(deps.storage, &status)?;
243
244    Ok(Response::new().add_attribute("action", "sudo_update_status"))
245}
246
247#[cfg_attr(not(feature = "library"), entry_point)]
248pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
249    match msg {
250        QueryMsg::Config {} => to_json_binary(&query_config(deps)?),
251        QueryMsg::Status {} => to_json_binary(&query_status(deps)?),
252    }
253}
254
255fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
256    let config = CONFIG.load(deps.storage)?;
257    let collection_address = COLLECTION_ADDRESS.load(deps.storage)?;
258
259    Ok(ConfigResponse {
260        collection_address: collection_address.to_string(),
261        config,
262    })
263}
264
265pub fn query_status(deps: Deps) -> StdResult<StatusResponse> {
266    let status = STATUS.load(deps.storage)?;
267
268    Ok(StatusResponse { status })
269}
270
271// Reply callback triggered from sg721 contract instantiation in instantiate()
272#[cfg_attr(not(feature = "library"), entry_point)]
273pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
274    if msg.id != INSTANTIATE_SG721_REPLY_ID {
275        return Err(ContractError::InvalidReplyID {});
276    }
277
278    let reply = parse_reply_instantiate_data(msg);
279    match reply {
280        Ok(res) => {
281            let collection_address = res.contract_address;
282            COLLECTION_ADDRESS.save(deps.storage, &Addr::unchecked(collection_address.clone()))?;
283            Ok(Response::default()
284                .add_attribute("action", "instantiate_sg721_reply")
285                .add_attribute("sg721_address", collection_address))
286        }
287        Err(_) => Err(ContractError::InstantiateSg721Error {}),
288    }
289}