use serde::de::DeserializeOwned;
use serde::Serialize;
use cosmwasm_std::{Binary, CustomMsg, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
use cw2::set_contract_version;
use cw_utils::maybe_addr;
use bs721::{ContractInfoResponse, Bs721Execute, Bs721ReceiveMsg, Expiration};
use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, MintMsg};
use crate::state::{Approval, Bs721Contract, TokenInfo};
const CONTRACT_NAME: &str = "crates.io:bs721-base";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
const MAX_SELLER_FEE: u16 = 10000;
impl<'a, T, C, E, Q> Bs721Contract<'a, T, C, E, Q>
where
T: Serialize + DeserializeOwned + Clone,
C: CustomMsg,
E: CustomMsg,
Q: CustomMsg,
{
pub fn instantiate(
&self,
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response<C>> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
let info = ContractInfoResponse {
name: msg.name,
symbol: msg.symbol,
uri: msg.uri,
};
self.contract_info.save(deps.storage, &info)?;
let minter = deps.api.addr_validate(&msg.minter)?;
self.minter.save(deps.storage, &minter)?;
Ok(Response::default())
}
pub fn execute(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg<T, E>,
) -> Result<Response<C>, ContractError> {
match msg {
ExecuteMsg::Mint(msg) => self.mint(deps, env, info, msg),
ExecuteMsg::Approve {
spender,
token_id,
expires,
} => self.approve(deps, env, info, spender, token_id, expires),
ExecuteMsg::Revoke { spender, token_id } => {
self.revoke(deps, env, info, spender, token_id)
}
ExecuteMsg::ApproveAll { operator, expires } => {
self.approve_all(deps, env, info, operator, expires)
}
ExecuteMsg::RevokeAll { operator } => self.revoke_all(deps, env, info, operator),
ExecuteMsg::TransferNft {
recipient,
token_id,
} => self.transfer_nft(deps, env, info, recipient, token_id),
ExecuteMsg::SendNft {
contract,
token_id,
msg,
} => self.send_nft(deps, env, info, contract, token_id, msg),
ExecuteMsg::Burn { token_id } => self.burn(deps, env, info, token_id),
ExecuteMsg::Extension { msg: _ } => Ok(Response::default()),
}
}
}
impl<'a, T, C, E, Q> Bs721Contract<'a, T, C, E, Q>
where
T: Serialize + DeserializeOwned + Clone,
C: CustomMsg,
E: CustomMsg,
Q: CustomMsg,
{
pub fn mint(
&self,
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: MintMsg<T>,
) -> Result<Response<C>, ContractError> {
let minter = self.minter.load(deps.storage)?;
if info.sender != minter {
return Err(ContractError::Unauthorized {});
}
if (msg.seller_fee_bps.is_some() && msg.payment_addr.is_none())
|| (msg.seller_fee_bps.is_none() && msg.payment_addr.is_some())
{
return Err(ContractError::InvalidSellerFee {});
}
if let Some(fee) = msg.seller_fee_bps {
if fee > MAX_SELLER_FEE {
return Err(ContractError::MaxSellerFeeExceeded {});
}
}
let token = TokenInfo {
owner: deps.api.addr_validate(&msg.owner)?,
approvals: vec![],
token_uri: msg.token_uri,
extension: msg.extension,
seller_fee_bps: msg.seller_fee_bps,
payment_addr: maybe_addr(deps.api, msg.payment_addr)?,
};
self.tokens
.update(deps.storage, &msg.token_id, |old| match old {
Some(_) => Err(ContractError::Claimed {}),
None => Ok(token),
})?;
self.increment_tokens(deps.storage)?;
Ok(Response::new()
.add_attribute("action", "mint")
.add_attribute("minter", info.sender)
.add_attribute("owner", msg.owner)
.add_attribute("token_id", msg.token_id))
}
}
impl<'a, T, C, E, Q> Bs721Execute<T, C> for Bs721Contract<'a, T, C, E, Q>
where
T: Serialize + DeserializeOwned + Clone,
C: CustomMsg,
E: CustomMsg,
Q: CustomMsg,
{
type Err = ContractError;
fn transfer_nft(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
recipient: String,
token_id: String,
) -> Result<Response<C>, ContractError> {
self._transfer_nft(deps, &env, &info, &recipient, &token_id)?;
Ok(Response::new()
.add_attribute("action", "transfer_nft")
.add_attribute("sender", info.sender)
.add_attribute("recipient", recipient)
.add_attribute("token_id", token_id))
}
fn send_nft(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
contract: String,
token_id: String,
msg: Binary,
) -> Result<Response<C>, ContractError> {
self._transfer_nft(deps, &env, &info, &contract, &token_id)?;
let send = Bs721ReceiveMsg {
sender: info.sender.to_string(),
token_id: token_id.clone(),
msg,
};
Ok(Response::new()
.add_message(send.into_cosmos_msg(contract.clone())?)
.add_attribute("action", "send_nft")
.add_attribute("sender", info.sender)
.add_attribute("recipient", contract)
.add_attribute("token_id", token_id))
}
fn approve(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
spender: String,
token_id: String,
expires: Option<Expiration>,
) -> Result<Response<C>, ContractError> {
self._update_approvals(deps, &env, &info, &spender, &token_id, true, expires)?;
Ok(Response::new()
.add_attribute("action", "approve")
.add_attribute("sender", info.sender)
.add_attribute("spender", spender)
.add_attribute("token_id", token_id))
}
fn revoke(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
spender: String,
token_id: String,
) -> Result<Response<C>, ContractError> {
self._update_approvals(deps, &env, &info, &spender, &token_id, false, None)?;
Ok(Response::new()
.add_attribute("action", "revoke")
.add_attribute("sender", info.sender)
.add_attribute("spender", spender)
.add_attribute("token_id", token_id))
}
fn approve_all(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
operator: String,
expires: Option<Expiration>,
) -> Result<Response<C>, ContractError> {
let expires = expires.unwrap_or_default();
if expires.is_expired(&env.block) {
return Err(ContractError::Expired {});
}
let operator_addr = deps.api.addr_validate(&operator)?;
self.operators
.save(deps.storage, (&info.sender, &operator_addr), &expires)?;
Ok(Response::new()
.add_attribute("action", "approve_all")
.add_attribute("sender", info.sender)
.add_attribute("operator", operator))
}
fn revoke_all(
&self,
deps: DepsMut,
_env: Env,
info: MessageInfo,
operator: String,
) -> Result<Response<C>, ContractError> {
let operator_addr = deps.api.addr_validate(&operator)?;
self.operators
.remove(deps.storage, (&info.sender, &operator_addr));
Ok(Response::new()
.add_attribute("action", "revoke_all")
.add_attribute("sender", info.sender)
.add_attribute("operator", operator))
}
fn burn(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
token_id: String,
) -> Result<Response<C>, ContractError> {
let token = self.tokens.load(deps.storage, &token_id)?;
self.check_can_send(deps.as_ref(), &env, &info, &token)?;
self.tokens.remove(deps.storage, &token_id)?;
self.decrement_tokens(deps.storage)?;
Ok(Response::new()
.add_attribute("action", "burn")
.add_attribute("sender", info.sender)
.add_attribute("token_id", token_id))
}
}
impl<'a, T, C, E, Q> Bs721Contract<'a, T, C, E, Q>
where
T: Serialize + DeserializeOwned + Clone,
C: CustomMsg,
E: CustomMsg,
Q: CustomMsg,
{
pub fn _transfer_nft(
&self,
deps: DepsMut,
env: &Env,
info: &MessageInfo,
recipient: &str,
token_id: &str,
) -> Result<TokenInfo<T>, ContractError> {
let mut token = self.tokens.load(deps.storage, token_id)?;
self.check_can_send(deps.as_ref(), env, info, &token)?;
token.owner = deps.api.addr_validate(recipient)?;
token.approvals = vec![];
self.tokens.save(deps.storage, token_id, &token)?;
Ok(token)
}
#[allow(clippy::too_many_arguments)]
pub fn _update_approvals(
&self,
deps: DepsMut,
env: &Env,
info: &MessageInfo,
spender: &str,
token_id: &str,
add: bool,
expires: Option<Expiration>,
) -> Result<TokenInfo<T>, ContractError> {
let mut token = self.tokens.load(deps.storage, token_id)?;
self.check_can_approve(deps.as_ref(), env, info, &token)?;
let spender_addr = deps.api.addr_validate(spender)?;
token.approvals.retain(|apr| apr.spender != spender_addr);
if add {
let expires = expires.unwrap_or_default();
if expires.is_expired(&env.block) {
return Err(ContractError::Expired {});
}
let approval = Approval {
spender: spender_addr,
expires,
};
token.approvals.push(approval);
}
self.tokens.save(deps.storage, token_id, &token)?;
Ok(token)
}
pub fn check_can_approve(
&self,
deps: Deps,
env: &Env,
info: &MessageInfo,
token: &TokenInfo<T>,
) -> Result<(), ContractError> {
if token.owner == info.sender {
return Ok(());
}
let op = self
.operators
.may_load(deps.storage, (&token.owner, &info.sender))?;
match op {
Some(ex) => {
if ex.is_expired(&env.block) {
Err(ContractError::Unauthorized {})
} else {
Ok(())
}
}
None => Err(ContractError::Unauthorized {}),
}
}
pub fn check_can_send(
&self,
deps: Deps,
env: &Env,
info: &MessageInfo,
token: &TokenInfo<T>,
) -> Result<(), ContractError> {
if token.owner == info.sender {
return Ok(());
}
if token
.approvals
.iter()
.any(|apr| apr.spender == info.sender && !apr.is_expired(&env.block))
{
return Ok(());
}
let op = self
.operators
.may_load(deps.storage, (&token.owner, &info.sender))?;
match op {
Some(ex) => {
if ex.is_expired(&env.block) {
Err(ContractError::Unauthorized {})
} else {
Ok(())
}
}
None => Err(ContractError::Unauthorized {}),
}
}
}