470 lines
14 KiB
Rust
470 lines
14 KiB
Rust
#![doc = include_str!("../README.md")]
|
||
#![allow(unused_variables)]
|
||
|
||
#[macro_use]
|
||
extern crate pbc_contract_codegen;
|
||
|
||
use pbc_contract_common::shortname::Shortname;
|
||
|
||
use pbc_contract_common::events::EventGroup;
|
||
use create_type_spec_derive::CreateTypeSpec;
|
||
use pbc_contract_common::address::Address;
|
||
use pbc_contract_common::avl_tree_map::{AvlTreeMap, AvlTreeSet};
|
||
use pbc_contract_common::context::ContractContext;
|
||
use read_write_state_derive::ReadWriteState;
|
||
|
||
use notamon_common::{NotamonAttributes, AssetId};
|
||
|
||
/// A permission to transfer and approve NFTs given from an NFT owner to a separate address, called an operator.
|
||
#[derive(ReadWriteState, CreateTypeSpec, PartialEq, Copy, Clone, Ord, PartialOrd, Eq)]
|
||
struct OperatorApproval {
|
||
/// NFT owner.
|
||
owner: Address,
|
||
/// Operator of the owner's tokens.
|
||
operator: Address,
|
||
}
|
||
|
||
/// Must correspond with [`NotamonId`].
|
||
///
|
||
/// ```
|
||
/// let _: TokenId = notamon_common::NotamonId { id : 1 }.id;
|
||
/// ```
|
||
type TokenId = u128;
|
||
|
||
/// Unit2
|
||
#[derive(CreateTypeSpec, ReadWriteState)]
|
||
pub struct Unit2 {}
|
||
|
||
/// State of the contract.
|
||
#[state]
|
||
pub struct NFTContractState {
|
||
/// Descriptive name for the collection of NFTs in this contract.
|
||
///
|
||
/// Required by MPC-721.
|
||
name: String,
|
||
/// Abbreviated name for NFTs in this contract.
|
||
///
|
||
/// Required by MPC-721.
|
||
symbol: String,
|
||
/// Mapping from token_id to the owner of the token.
|
||
///
|
||
/// Required by MPC-721.
|
||
owners: AvlTreeMap<TokenId, Address>,
|
||
/// Mapping from token_id to the approved address who can transfer the token.
|
||
///
|
||
/// Required by MPC-721.
|
||
token_approvals: AvlTreeMap<TokenId, Address>,
|
||
/// Containing approved operators of owners. Operators can transfer and change approvals on all tokens owned by owner.
|
||
///
|
||
/// Required by MPC-721.
|
||
operator_approvals: AvlTreeMap<OperatorApproval, Unit2>,
|
||
/// Template which the uri's of the NFTs fit into.
|
||
///
|
||
/// Required by MPC-721.
|
||
uri_template: String,
|
||
/// Mapping from token_id to the URI of the token.
|
||
///
|
||
/// Required by MPC-721.
|
||
token_uri_details: AvlTreeMap<TokenId, String>,
|
||
/// Owner of the contract. Is allowed to mint new NFTs.
|
||
///
|
||
/// Required by MPC-721.
|
||
contract_owner: Address,
|
||
|
||
/// Notamon attributes
|
||
notamon_attributes: AvlTreeMap<TokenId, NotamonAttributes>,
|
||
|
||
cached_asset_unlocks: AvlTreeSet<AssetId>,
|
||
|
||
asset_contract: Address,
|
||
}
|
||
|
||
impl NFTContractState {
|
||
/// Find the owner of an NFT.
|
||
/// Throws if no such token exists.
|
||
///
|
||
/// ### Parameters:
|
||
///
|
||
/// * `token_id`: [`TokenId`] The identifier for an NFT.
|
||
///
|
||
/// ### Returns:
|
||
///
|
||
/// An [`Address`] for the owner of the NFT.
|
||
pub fn owner_of(&self, token_id: TokenId) -> Address {
|
||
self.owners
|
||
.get(&token_id)
|
||
.expect("MPC-721: owner query for nonexistent token")
|
||
}
|
||
|
||
/// Get the approved address for a single NFT.
|
||
///
|
||
/// ### Parameters:
|
||
///
|
||
/// * `token_id`: [`TokenId`] The NFT to find the approved address for.
|
||
///
|
||
/// ### Returns:
|
||
///
|
||
/// An [`Option<Address>`] The approved address for this NFT, or none if there is none.
|
||
pub fn get_approved(&self, token_id: TokenId) -> Option<Address> {
|
||
self.token_approvals.get(&token_id)
|
||
}
|
||
|
||
/// Query if an address is an authorized operator for another address.
|
||
///
|
||
/// ### Parameters:
|
||
///
|
||
/// * `owner`: [`Address`] The address that owns the NFTs.
|
||
///
|
||
/// * `operator`: [`Address`] The address that acts on behalf of the owner.
|
||
///
|
||
/// ### Returns:
|
||
///
|
||
/// A [`bool`] true if `operator` is an approved operator for `owner`, false otherwise.
|
||
pub fn is_approved_for_all(&self, owner: Address, operator: Address) -> bool {
|
||
let as_operator_approval: OperatorApproval = OperatorApproval { owner, operator };
|
||
self.operator_approvals.contains_key(&as_operator_approval)
|
||
}
|
||
|
||
/// Helper function to check whether a tokenId exists.
|
||
///
|
||
/// Tokens start existing when they are minted (`mint`),
|
||
/// and stop existing when they are burned (`burn`).
|
||
///
|
||
/// ### Parameters:
|
||
///
|
||
/// * `token_id`: [`TokenId`] The tokenId that is checked.
|
||
///
|
||
/// ### Returns:
|
||
///
|
||
/// A [`bool`] True if `token_id` is in use, false otherwise.
|
||
pub fn exists(&self, token_id: TokenId) -> bool {
|
||
self.owners.contains_key(&token_id)
|
||
}
|
||
|
||
/// Helper function to check whether a spender is owner or approved for a given token.
|
||
/// Throws if token_id does not exist.
|
||
///
|
||
/// ### Parameters:
|
||
///
|
||
/// * `spender`: [`Address`] The address to check ownership for.
|
||
///
|
||
/// * `token_id`: [`TokenId`] The tokenId which is checked.
|
||
///
|
||
/// ### Returns:
|
||
///
|
||
/// A [`bool`] True if `token_id` is owned or approved for `spender`, false otherwise.
|
||
pub fn is_approved_or_owner(&self, spender: Address, token_id: TokenId) -> bool {
|
||
let owner = self.owner_of(token_id);
|
||
spender == owner
|
||
|| self.get_approved(token_id) == Some(spender)
|
||
|| self.is_approved_for_all(owner, spender)
|
||
}
|
||
|
||
/// Mutates the state by approving `to` to operate on `token_id`.
|
||
/// None indicates there is no approved address.
|
||
///
|
||
/// ### Parameters:
|
||
///
|
||
/// * `approved`: [`Option<Address>`], The new approved NFT controller.
|
||
///
|
||
/// * `token_id`: [`TokenId`], The NFT to approve.
|
||
pub fn _approve(&mut self, approved: Option<Address>, token_id: TokenId) {
|
||
if let Some(appr) = approved {
|
||
self.token_approvals.insert(token_id, appr);
|
||
} else {
|
||
self.token_approvals.remove(&token_id);
|
||
}
|
||
}
|
||
|
||
/// Mutates the state by transferring `token_id` from `from` to `to`.
|
||
/// As opposed to {transfer_from}, this imposes no restrictions on `ctx.sender`.
|
||
///
|
||
/// Throws if `from` is not the owner of `token_id`.
|
||
///
|
||
/// ### Parameters:
|
||
///
|
||
/// * `from`: [`Address`], The current owner of the NFT
|
||
///
|
||
/// * `to`: [`Address`], The new owner
|
||
///
|
||
/// * `token_id`: [`TokenId`], The NFT to transfer
|
||
pub fn _transfer(&mut self, from: Address, to: Address, token_id: TokenId) {
|
||
if self.owner_of(token_id) != from {
|
||
panic!("MPC-721: transfer from incorrect owner")
|
||
} else {
|
||
// clear approvals from the previous owner
|
||
self._approve(None, token_id);
|
||
self.owners.insert(token_id, to);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Create a new NFT contract, with a name, symbol and URI template. No NFTs are minted initially.
|
||
///
|
||
/// ### Parameters:
|
||
///
|
||
/// * `ctx`: [`ContractContext`], initial context.
|
||
///
|
||
/// * `name`: [`String`], A descriptive name for a collection of NFTs in this contract.
|
||
///
|
||
/// * `symbol`: [`String`], An abbreviated name for NFTs in this contract.
|
||
///
|
||
/// * `uri_template`: [`String`], Template for uri´s associated with NFTs in this contract.
|
||
///
|
||
/// ### Returns:
|
||
///
|
||
/// The new state object of type [`NFTContractState`].
|
||
#[init]
|
||
pub fn initialize(
|
||
ctx: ContractContext,
|
||
name: String,
|
||
symbol: String,
|
||
uri_template: String,
|
||
asset_contract: Address,
|
||
) -> NFTContractState {
|
||
NFTContractState {
|
||
name,
|
||
symbol,
|
||
owners: AvlTreeMap::new(),
|
||
token_approvals: AvlTreeMap::new(),
|
||
operator_approvals: AvlTreeMap::new(),
|
||
uri_template,
|
||
token_uri_details: AvlTreeMap::new(),
|
||
contract_owner: ctx.sender,
|
||
notamon_attributes: AvlTreeMap::new(),
|
||
cached_asset_unlocks: AvlTreeSet::new(),
|
||
asset_contract,
|
||
}
|
||
}
|
||
|
||
/// User allows another user or contract to [`transfer_from`] their NFT. Each NFT can only have a single approved account.
|
||
///
|
||
/// Change or reaffirm the approved address for an NFT.
|
||
/// `approved==None` revokes any existing approved address.
|
||
/// Throws unless `ctx.sender` is the current NFT owner, or an authorized
|
||
/// operator of the current owner.
|
||
///
|
||
/// ### Parameters:
|
||
///
|
||
/// * `ctx`: [`ContractContext`], the context for the action call.
|
||
///
|
||
/// * `state`: [`NFTContractState`], the current state of the contract.
|
||
///
|
||
/// * `approved`: [`Option<Address>`], The new approved NFT controller.
|
||
///
|
||
/// * `token_id`: [`TokenId`], The NFT to approve.
|
||
///
|
||
/// ### Returns
|
||
///
|
||
/// The new state object of type [`NFTContractState`] with an updated ledger.
|
||
#[action(shortname = 0x05)]
|
||
pub fn approve(
|
||
ctx: ContractContext,
|
||
mut state: NFTContractState,
|
||
approved: Option<Address>,
|
||
token_id: TokenId,
|
||
) -> NFTContractState {
|
||
let owner = state.owner_of(token_id);
|
||
if ctx.sender != owner && !state.is_approved_for_all(owner, ctx.sender) {
|
||
panic!("MPC-721: approve caller is not owner nor authorized operator")
|
||
}
|
||
state._approve(approved, token_id);
|
||
state
|
||
}
|
||
|
||
/// User allows another user or contract ("operator") to manage all of their NFTs for them. Operators can do everything the real owner can.
|
||
/// Each token can only have a single operator.
|
||
///
|
||
/// Throws if `operator` == `ctx.sender`.
|
||
///
|
||
/// ### Parameters:
|
||
///
|
||
/// * `context`: [`ContractContext`], the context for the action call.
|
||
///
|
||
/// * `state`: [`NFTContractState`], the current state of the contract.
|
||
///
|
||
/// * `operator`: [`Address`], Address to add to the set of authorized operators.
|
||
///
|
||
/// * `approved`: [`bool`], True if the operator is approved, false to revoke approval.
|
||
///
|
||
/// ### Returns
|
||
///
|
||
/// The new state object of type [`NFTContractState`] with an updated ledger.
|
||
#[action(shortname = 0x07)]
|
||
pub fn set_approval_for_all(
|
||
ctx: ContractContext,
|
||
mut state: NFTContractState,
|
||
operator: Address,
|
||
approved: bool,
|
||
) -> NFTContractState {
|
||
if operator == ctx.sender {
|
||
panic!("MPC-721: approve to caller")
|
||
}
|
||
let operator_approval = OperatorApproval {
|
||
owner: ctx.sender,
|
||
operator,
|
||
};
|
||
if approved {
|
||
state.operator_approvals.insert(operator_approval, Unit2 {});
|
||
} else {
|
||
state.operator_approvals.remove(&operator_approval);
|
||
}
|
||
state
|
||
}
|
||
|
||
/// Transfer ownership of an NFT to recipient.
|
||
///
|
||
/// User must have permission to perform the transfer: Throws unless `ctx.sender` is the current owner, an authorized
|
||
/// operator, or the approved address for this NFT. Throws if `from` is
|
||
/// not the current owner. Throws if `token_id` is not a valid NFT.
|
||
///
|
||
/// ### Parameters:
|
||
///
|
||
/// * `ctx`: [`ContractContext`], the context for the action call.
|
||
///
|
||
/// * `state`: [`NFTContractState`], the current state of the contract.
|
||
///
|
||
/// * `from`: [`Address`], The current owner of the NFT
|
||
///
|
||
/// * `to`: [`Address`], The new owner
|
||
///
|
||
/// * `token_id`: [`TokenId`], The NFT to transfer
|
||
///
|
||
/// ### Returns
|
||
///
|
||
/// The new state object of type [`NFTContractState`] with an updated ledger.
|
||
#[action(shortname = 0x03)]
|
||
pub fn transfer_from(
|
||
ctx: ContractContext,
|
||
mut state: NFTContractState,
|
||
from: Address,
|
||
to: Address,
|
||
token_id: TokenId,
|
||
) -> NFTContractState {
|
||
if !state.is_approved_or_owner(ctx.sender, token_id) {
|
||
panic!("MPC-721: transfer caller is not owner nor approved")
|
||
} else {
|
||
state._transfer(from, to, token_id);
|
||
state
|
||
}
|
||
}
|
||
|
||
/// Create a new NFT, and send it to the specified recipient. Only the contract owner can use this.
|
||
///
|
||
/// Requirements:
|
||
///
|
||
/// - `token_id` must not exist
|
||
/// - `ctx.sender` owns the contract
|
||
///
|
||
/// ### Parameters:
|
||
///
|
||
/// * `ctx`: [`ContractContext`], the context for the action call.
|
||
///
|
||
/// * `state`: [`NFTContractState`], the current state of the contract.
|
||
///
|
||
/// * `to`: [`Address`], the owner of the minted token.
|
||
///
|
||
/// * `token_id`: [`TokenId`], The new id for the minted token.
|
||
///
|
||
/// ### Returns
|
||
///
|
||
/// The new state object of type [`NFTContractState`] with an updated ledger.
|
||
#[action(shortname = 0x01)]
|
||
pub fn mint(
|
||
ctx: ContractContext,
|
||
mut state: NFTContractState,
|
||
to: Address,
|
||
token_id: TokenId,
|
||
token_uri: String,
|
||
) -> NFTContractState {
|
||
panic!("Default NFT minting is not supported")
|
||
}
|
||
|
||
fn unlock_assets(
|
||
ctx: ContractContext,
|
||
mut state: NFTContractState,
|
||
notamon_attributes: NotamonAttributes,
|
||
) -> (
|
||
NFTContractState,
|
||
Vec<EventGroup>,
|
||
) {
|
||
let ids_of_not_yet_unlocked_assets: Vec<AssetId> = notamon_attributes.asset_ids().into_iter().filter(
|
||
|id| !state.cached_asset_unlocks.contains(id)
|
||
) .collect();
|
||
|
||
if ids_of_not_yet_unlocked_assets.is_empty() {
|
||
return (state, vec![]);
|
||
}
|
||
|
||
for id in ids_of_not_yet_unlocked_assets.clone() {
|
||
state.cached_asset_unlocks.insert(id)
|
||
}
|
||
|
||
let mut event_group_builder = EventGroup::builder();
|
||
event_group_builder
|
||
.call(state.asset_contract, Shortname::from_u32(0x07))
|
||
.argument(ids_of_not_yet_unlocked_assets)
|
||
.done();
|
||
|
||
(
|
||
state,
|
||
vec![event_group_builder.build()]
|
||
)
|
||
}
|
||
|
||
#[action(shortname = 0x10)]
|
||
pub fn mint_notamon(
|
||
ctx: ContractContext,
|
||
mut state: NFTContractState,
|
||
to: Address,
|
||
token_id: TokenId,
|
||
notamon_attributes: NotamonAttributes,
|
||
) -> (
|
||
NFTContractState,
|
||
Vec<EventGroup>,
|
||
) {
|
||
if ctx.sender != state.contract_owner {
|
||
panic!("MPC-721: mint only callable by the contract owner")
|
||
} else if state.exists(token_id) {
|
||
panic!("MPC-721: token already minted")
|
||
} else {
|
||
let token_uri: String = "TODO".into();
|
||
state.owners.insert(token_id, to);
|
||
state.notamon_attributes.insert(token_id, notamon_attributes.clone());
|
||
state.token_uri_details.insert(token_id, token_uri);
|
||
unlock_assets(ctx, state, notamon_attributes)
|
||
}
|
||
}
|
||
|
||
/// Destroy a NFT with id `token_id`.
|
||
///
|
||
/// Requires that the `token_id` exists and `ctx.sender` is approved or owner of the token.
|
||
///
|
||
/// The approval is cleared when the token is burned.
|
||
///
|
||
/// ### Parameters:
|
||
///
|
||
/// * `ctx`: [`ContractContext`], the context for the action call.
|
||
///
|
||
/// * `state`: [`NFTContractState`], the current state of the contract.
|
||
///
|
||
/// * `token_id`: [`TokenId`], The id of the NFT to be burned.
|
||
///
|
||
/// ### Returns
|
||
///
|
||
/// The new state object of type [`NFTContractState`] with an updated ledger.
|
||
#[action(shortname = 0x08)]
|
||
pub fn burn(ctx: ContractContext, mut state: NFTContractState, token_id: TokenId) -> NFTContractState {
|
||
if !state.is_approved_or_owner(ctx.sender, token_id) {
|
||
panic!("MPC-721: burn caller is not owner nor approved")
|
||
} else {
|
||
let owner = state.owner_of(token_id);
|
||
// Clear approvals
|
||
state._approve(None, token_id);
|
||
|
||
state.owners.remove(&token_id);
|
||
state.token_uri_details.remove(&token_id);
|
||
state
|
||
}
|
||
}
|