diff --git a/README.md b/README.md index e69de29..1bd9943 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,5 @@ +# Notamon Game Engine + +Base game engine for the Notamon game project for the [Partisia Blockchain](https://github.com/partisiablockchain). + +Work in Progress. diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 73e4006..9fba9c8 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "notamon-common", + "notamon-nft", "notamon-asset-contract", ] diff --git a/rust/notamon-asset-contract/README.md b/rust/notamon-asset-contract/README.md index e69de29..2f3b9ae 100644 --- a/rust/notamon-asset-contract/README.md +++ b/rust/notamon-asset-contract/README.md @@ -0,0 +1,7 @@ +# Notamon Asset Contract + +Used to store game assets as secret-shares, which prevent data mining. + +Based on the [File Share Example +Contract](https://github.com/partisiablockchain/example-contracts/tree/main/rust/zk-file-share), +with extended permission systems and ability to publish assets on-chain. diff --git a/rust/notamon-common/src/lib.rs b/rust/notamon-common/src/lib.rs index 1fc5adc..616590c 100644 --- a/rust/notamon-common/src/lib.rs +++ b/rust/notamon-common/src/lib.rs @@ -1,17 +1,12 @@ #![doc = include_str!("../README.md")] -#![allow(unused_variables)] -use pbc_contract_common::avl_tree_map::AvlTreeMap; -use pbc_contract_common::address::Address; -use pbc_contract_common::context::ContractContext; -use pbc_contract_common::events::EventGroup; -use pbc_contract_common::zk::{SecretVarId, ZkInputDef, ZkState, ZkStateChange}; -use pbc_traits::{ReadRPC, WriteRPC, ReadWriteState}; use read_write_rpc_derive::ReadWriteRPC; use read_write_state_derive::ReadWriteState; use create_type_spec_derive::CreateTypeSpec; use std::fmt::Debug; +mod permission; +pub use permission::{Permission, Permissions}; #[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)] pub struct AssetId{ @@ -19,54 +14,34 @@ pub struct AssetId{ } #[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)] -pub enum Permission { - #[discriminant(0)] - None {}, - #[discriminant(1)] - Addresses { addresses: Vec
}, +pub struct NotamonId{ + id: u128, } -impl Permission { - pub const NONE: Permission = Permission::None { }; - - pub fn only(address: Address) -> Permission { - Permission::Addresses { addresses: vec![address] } - } - - pub fn allows(&self, address: &Address) -> bool { - match self { - Permission::None { } => false, - Permission::Addresses { addresses } => addresses.contains(address), - } - } +#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)] +pub struct SpeciesId{ + id: u16, } -#[derive(ReadWriteState, Debug, CreateTypeSpec)] -pub struct Permissions { - permissions: AvlTreeMap, +#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)] +pub struct SkinId{ + id: u8, } -impl Permissions { - - pub fn new () -> Self { - Self { permissions: AvlTreeMap::new() } - } - - pub fn set_permission(&mut self, permission_key: KeyT, permission: Permission) { - self.permissions.insert(permission_key, permission); - } - - pub fn get_permission(&self, permission: &KeyT) -> Permission { - self.permissions.get(permission).unwrap_or(Permission::NONE) - } - +#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)] +pub struct EffectId{ + id: u8, } -impl Permissions { - pub fn assert_has_permission(&self, address: &Address, permission: KeyT) { - if !self.get_permission(&permission).allows(address) { - panic!("User {address} does not have permission: {permission:?}"); - } - } -} +type StatAmount = u8; + +#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec)] +pub struct NotamonAttributes{ + species_id: SpeciesId, + skin_id: SkinId, + effect_id: EffectId, + stat_hp: StatAmount, + stat_attack: StatAmount, + stat_defense: StatAmount, +} diff --git a/rust/notamon-common/src/permission.rs b/rust/notamon-common/src/permission.rs new file mode 100644 index 0000000..0ecc752 --- /dev/null +++ b/rust/notamon-common/src/permission.rs @@ -0,0 +1,61 @@ +use pbc_contract_common::avl_tree_map::AvlTreeMap; +use pbc_contract_common::address::Address; +use pbc_traits::{ReadRPC, WriteRPC, ReadWriteState}; +use read_write_rpc_derive::ReadWriteRPC; +use read_write_state_derive::ReadWriteState; +use create_type_spec_derive::CreateTypeSpec; +use std::fmt::Debug; + +#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)] +pub enum Permission { + #[discriminant(0)] + None {}, + #[discriminant(1)] + Addresses { addresses: Vec
}, +} + +impl Permission { + pub const NONE: Permission = Permission::None { }; + + pub fn only(address: Address) -> Permission { + Permission::Addresses { addresses: vec![address] } + } + + pub fn allows(&self, address: &Address) -> bool { + match self { + Permission::None { } => false, + Permission::Addresses { addresses } => addresses.contains(address), + } + } +} + +#[derive(ReadWriteState, Debug, CreateTypeSpec)] +pub struct Permissions { + permissions: AvlTreeMap, +} + +impl Permissions { + + pub fn new () -> Self { + Self { permissions: AvlTreeMap::new() } + } + + pub fn set_permission(&mut self, permission_key: KeyT, permission: Permission) { + self.permissions.insert(permission_key, permission); + } + + pub fn get_permission(&self, permission: &KeyT) -> Permission { + self.permissions.get(permission).unwrap_or(Permission::NONE) + } + +} + +impl Permissions { + pub fn assert_has_permission(&self, address: &Address, permission: KeyT) { + if !self.get_permission(&permission).allows(address) { + panic!("User {address} does not have permission: {permission:?}"); + } + } +} + + diff --git a/rust/notamon-nft/Cargo.toml b/rust/notamon-nft/Cargo.toml new file mode 100644 index 0000000..fe07eb2 --- /dev/null +++ b/rust/notamon-nft/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "nft-v2-contract" +readme = "README.md" +version.workspace = true +description.workspace = true +homepage.workspace = true +repository.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +crate-type = ['rlib', 'cdylib'] + +[dependencies] +notamon-common = { path = "../notamon-common" } + +pbc_contract_common.workspace = true +pbc_traits.workspace = true +pbc_lib.workspace = true +read_write_rpc_derive.workspace = true +read_write_state_derive.workspace = true +create_type_spec_derive.workspace = true +pbc_contract_codegen.workspace = true + +[features] +abi = ["pbc_contract_common/abi", "pbc_contract_codegen/abi", "pbc_traits/abi", "create_type_spec_derive/abi", "pbc_lib/abi"] diff --git a/rust/notamon-nft/README.md b/rust/notamon-nft/README.md new file mode 100644 index 0000000..b2d2303 --- /dev/null +++ b/rust/notamon-nft/README.md @@ -0,0 +1,6 @@ +# Notamon NFT Contract + +Stores the Notamons themselves with their various statistics and +characteristics. + +Based on the [NFT-v2 Example Contract](https://github.com/partisiablockchain/defi/tree/main/rust/nft-v2). diff --git a/rust/notamon-nft/nft-v2/Cargo.toml b/rust/notamon-nft/nft-v2/Cargo.toml new file mode 100644 index 0000000..330eba9 --- /dev/null +++ b/rust/notamon-nft/nft-v2/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "nft-v2-contract" +readme = "README.md" +version.workspace = true +description.workspace = true +homepage.workspace = true +repository.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +crate-type = ['rlib', 'cdylib'] + +[dependencies] +pbc_contract_common.workspace = true +pbc_traits.workspace = true +pbc_lib.workspace = true +read_write_rpc_derive.workspace = true +read_write_state_derive.workspace = true +create_type_spec_derive.workspace = true +pbc_contract_codegen.workspace = true + +[features] +abi = ["pbc_contract_common/abi", "pbc_contract_codegen/abi", "pbc_traits/abi", "create_type_spec_derive/abi", "pbc_lib/abi"] diff --git a/rust/notamon-nft/nft-v2/README.md b/rust/notamon-nft/nft-v2/README.md new file mode 100644 index 0000000..ccd8047 --- /dev/null +++ b/rust/notamon-nft/nft-v2/README.md @@ -0,0 +1,42 @@ +# NFT v2 (MPC-721-v2) + +An example of a NFT (Non-Fungible Token) smart contract for Partisia +Blockchain, implementing the MPC-721-v2 standard. + +## Background, NFT + +An NFT is a unique identifier managed by an NFT contract, that can be +transfered between accounts on the blockchain. NFTs can be used in much the +same way as [MPC-20 tokens](../token-v2) can, but NFTs represent specific +instances of an object (non-fungible like a physical book; there are many like +it, but the one sitting on your bookshelf is yours and has a history), whereas +[tokens](../token-v2) are interchangable (fungible; like money in a bank +account). + +NFTs are often associated with specific artworks, which are publically +accessible by a unique link stored in the contract; artwork is rarely stored +on-chain. + +Some NFT contracts also manage additional attributes associated with each NFT, +for example their history of ownership. This functionality is not implemented +by `nft-v2`. + +## Implementation + +This example follows the mpc-721-v2 standard contract interface. You can read more about this standard here: [https://partisiablockchain.gitlab.io/documentation/smart-contracts/integration/mpc-721-nft-contract.html](https://partisiablockchain.gitlab.io/documentation/smart-contracts/integration/mpc-721-nft-contract.html) + +The contract is inspired by the ERC721 NFT contract with extensions for Metadata and Burnable\ +[https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md) + +## Extensions + +This contract is meant as a jumping off point to making your own NFTs. Here are +some ideas: + +- NFT attributes: Track anything you want! This can include ownership history, + rarity, game stats, etc. +- On-chain generation: Partisia Blockchain REAL/ZK allows for true randomness. + Generate your attributes on-chain and store your images off-chain. +- User-requested minting: With on-chain generation your can allow your users to + mint their own NFTs. Then you can limit them to a certain amount, or let them + run amok. diff --git a/rust/notamon-nft/nft-v2/src/lib.rs b/rust/notamon-nft/nft-v2/src/lib.rs new file mode 100644 index 0000000..ff32562 --- /dev/null +++ b/rust/notamon-nft/nft-v2/src/lib.rs @@ -0,0 +1,382 @@ +#![doc = include_str!("../README.md")] +#![allow(unused_variables)] + +#[macro_use] +extern crate pbc_contract_codegen; + +use create_type_spec_derive::CreateTypeSpec; +use pbc_contract_common::address::Address; +use pbc_contract_common::avl_tree_map::AvlTreeMap; +use pbc_contract_common::context::ContractContext; +use read_write_state_derive::ReadWriteState; + +/// 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, +} + +/// Unit +#[derive(CreateTypeSpec, ReadWriteState)] +pub struct Unit {} + +/// State of the contract. +#[state] +pub struct NFTContractState { + /// Descriptive name for the collection of NFTs in this contract. + name: String, + /// Abbreviated name for NFTs in this contract. + symbol: String, + /// Mapping from token_id to the owner of the token. + owners: AvlTreeMap, + /// Mapping from token_id to the approved address who can transfer the token. + token_approvals: AvlTreeMap, + /// Containing approved operators of owners. Operators can transfer and change approvals on all tokens owned by owner. + operator_approvals: AvlTreeMap, + /// Template which the uri's of the NFTs fit into. + uri_template: String, + /// Mapping from token_id to the URI of the token. + token_uri_details: AvlTreeMap, + /// Owner of the contract. Is allowed to mint new NFTs. + contract_owner: Address, +} + +impl NFTContractState { + /// Find the owner of an NFT. + /// Throws if no such token exists. + /// + /// ### Parameters: + /// + /// * `token_id`: [`u128`] The identifier for an NFT. + /// + /// ### Returns: + /// + /// An [`Address`] for the owner of the NFT. + pub fn owner_of(&self, token_id: u128) -> 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`: [`u128`] 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: u128) -> 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`: [`u128`] The tokenId that is checked. + /// + /// ### Returns: + /// + /// A [`bool`] True if `token_id` is in use, false otherwise. + pub fn exists(&self, token_id: u128) -> 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`: [`u128`] 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: u128) -> 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`: [`u128`], The NFT to approve. + pub fn _approve(&mut self, approved: Option
, token_id: u128) { + 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`: [`u128`], The NFT to transfer + pub fn _transfer(&mut self, from: Address, to: Address, token_id: u128) { + 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, +) -> 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, + } +} + +/// 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`: [`u128`], 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: u128, +) -> 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, Unit {}); + } 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`: [`u128`], 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: u128, +) -> 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`: [`u128`], 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: u128, + token_uri: String, +) -> NFTContractState { + 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 { + state.owners.insert(token_id, to); + state.token_uri_details.insert(token_id, token_uri); + state + } +} + +/// 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`: [`u128`], 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: u128) -> 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 + } +} diff --git a/rust/notamon-nft/src/lib.rs b/rust/notamon-nft/src/lib.rs new file mode 100644 index 0000000..83da108 --- /dev/null +++ b/rust/notamon-nft/src/lib.rs @@ -0,0 +1,411 @@ +#![doc = include_str!("../README.md")] +#![allow(unused_variables)] + +#[macro_use] +extern crate pbc_contract_codegen; + +use create_type_spec_derive::CreateTypeSpec; +use pbc_contract_common::address::Address; +use pbc_contract_common::avl_tree_map::AvlTreeMap; +use pbc_contract_common::context::ContractContext; +use read_write_state_derive::ReadWriteState; + +use notamon_common::NotamonAttributes; + +/// 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; + +/// Unit +#[derive(CreateTypeSpec, ReadWriteState)] +pub struct Unit {} + +/// 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, +} + +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, +) -> 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(), + } +} + +/// 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, Unit {}); + } 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 { + 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 { + state.owners.insert(token_id, to); + state.token_uri_details.insert(token_id, token_uri); + state + } +} + +/// 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 + } +}