#![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, /// Mapping from token_id to the approved address who can transfer the token. /// /// Required by MPC-721. token_approvals: AvlTreeMap, /// Containing approved operators of owners. Operators can transfer and change approvals on all tokens owned by owner. /// /// Required by MPC-721. operator_approvals: AvlTreeMap, /// 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, /// Owner of the contract. Is allowed to mint new NFTs. /// /// Required by MPC-721. contract_owner: Address, /// Notamon attributes notamon_attributes: AvlTreeMap, cached_asset_unlocks: AvlTreeSet, 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
`] The approved address for this NFT, or none if there is none. pub fn get_approved(&self, token_id: TokenId) -> Option
{ 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
`], The new approved NFT controller. /// /// * `token_id`: [`TokenId`], The NFT to approve. pub fn _approve(&mut self, approved: Option
, 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
`], 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
, 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, ) { let ids_of_not_yet_unlocked_assets: Vec = 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, ) { 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 } }