394 lines
12 KiB
Rust
394 lines
12 KiB
Rust
use serde::de::DeserializeOwned;
|
|
use serde::Serialize;
|
|
|
|
use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
|
|
|
|
use cw2::set_contract_version;
|
|
use cw721::{ContractInfoResponse, CustomMsg, Cw721Execute, Cw721ReceiveMsg, Expiration};
|
|
|
|
use crate::error::ContractError;
|
|
use crate::msg::{ExecuteMsg, InstantiateMsg, MintMsg};
|
|
use crate::state::{Approval, Cw721Contract, TokenInfo};
|
|
|
|
// version info for migration info
|
|
const CONTRACT_NAME: &str = "crates.io:cw721-base";
|
|
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
|
|
impl<'a, T, C> Cw721Contract<'a, T, C>
|
|
where
|
|
T: Serialize + DeserializeOwned + Clone,
|
|
C: 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,
|
|
};
|
|
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>,
|
|
) -> 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),
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO pull this into some sort of trait extension??
|
|
impl<'a, T, C> Cw721Contract<'a, T, C>
|
|
where
|
|
T: Serialize + DeserializeOwned + Clone,
|
|
C: 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 {});
|
|
}
|
|
|
|
// create the token
|
|
let token = TokenInfo {
|
|
owner: deps.api.addr_validate(&msg.owner)?,
|
|
approvals: vec![],
|
|
token_uri: msg.token_uri,
|
|
extension: msg.extension,
|
|
};
|
|
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("token_id", msg.token_id))
|
|
}
|
|
}
|
|
|
|
impl<'a, T, C> Cw721Execute<T, C> for Cw721Contract<'a, T, C>
|
|
where
|
|
T: Serialize + DeserializeOwned + Clone,
|
|
C: 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> {
|
|
// Transfer token
|
|
self._transfer_nft(deps, &env, &info, &contract, &token_id)?;
|
|
|
|
let send = Cw721ReceiveMsg {
|
|
sender: info.sender.to_string(),
|
|
token_id: token_id.clone(),
|
|
msg,
|
|
};
|
|
|
|
// Send message
|
|
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> {
|
|
// reject expired data as invalid
|
|
let expires = expires.unwrap_or_default();
|
|
if expires.is_expired(&env.block) {
|
|
return Err(ContractError::Expired {});
|
|
}
|
|
|
|
// set the operator for us
|
|
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))
|
|
}
|
|
}
|
|
|
|
// helpers
|
|
impl<'a, T, C> Cw721Contract<'a, T, C>
|
|
where
|
|
T: Serialize + DeserializeOwned + Clone,
|
|
C: 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)?;
|
|
// ensure we have permissions
|
|
self.check_can_send(deps.as_ref(), env, info, &token)?;
|
|
// set owner and remove existing approvals
|
|
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,
|
|
// if add == false, remove. if add == true, remove then set with this expiration
|
|
add: bool,
|
|
expires: Option<Expiration>,
|
|
) -> Result<TokenInfo<T>, ContractError> {
|
|
let mut token = self.tokens.load(deps.storage, token_id)?;
|
|
// ensure we have permissions
|
|
self.check_can_approve(deps.as_ref(), env, info, &token)?;
|
|
|
|
// update the approval list (remove any for the same spender before adding)
|
|
let spender_addr = deps.api.addr_validate(spender)?;
|
|
token.approvals.retain(|apr| apr.spender != spender_addr);
|
|
|
|
// only difference between approve and revoke
|
|
if add {
|
|
// reject expired data as invalid
|
|
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)
|
|
}
|
|
|
|
/// returns true iff the sender can execute approve or reject on the contract
|
|
pub fn check_can_approve(
|
|
&self,
|
|
deps: Deps,
|
|
env: &Env,
|
|
info: &MessageInfo,
|
|
token: &TokenInfo<T>,
|
|
) -> Result<(), ContractError> {
|
|
// owner can approve
|
|
if token.owner == info.sender {
|
|
return Ok(());
|
|
}
|
|
// operator can approve
|
|
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 {}),
|
|
}
|
|
}
|
|
|
|
/// returns true iff the sender can transfer ownership of the token
|
|
pub fn check_can_send(
|
|
&self,
|
|
deps: Deps,
|
|
env: &Env,
|
|
info: &MessageInfo,
|
|
token: &TokenInfo<T>,
|
|
) -> Result<(), ContractError> {
|
|
// owner can send
|
|
if token.owner == info.sender {
|
|
return Ok(());
|
|
}
|
|
|
|
// any non-expired token approval can send
|
|
if token
|
|
.approvals
|
|
.iter()
|
|
.any(|apr| apr.spender == info.sender && !apr.is_expired(&env.block))
|
|
{
|
|
return Ok(());
|
|
}
|
|
|
|
// operator can send
|
|
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 {}),
|
|
}
|
|
}
|
|
}
|