Notamon NFT WIP
This commit is contained in:
parent
e39d48da82
commit
275495c8e8
|
@ -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.
|
|
@ -3,6 +3,7 @@ resolver = "2"
|
||||||
|
|
||||||
members = [
|
members = [
|
||||||
"notamon-common",
|
"notamon-common",
|
||||||
|
"notamon-nft",
|
||||||
"notamon-asset-contract",
|
"notamon-asset-contract",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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.
|
|
@ -1,17 +1,12 @@
|
||||||
#![doc = include_str!("../README.md")]
|
#![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_rpc_derive::ReadWriteRPC;
|
||||||
use read_write_state_derive::ReadWriteState;
|
use read_write_state_derive::ReadWriteState;
|
||||||
use create_type_spec_derive::CreateTypeSpec;
|
use create_type_spec_derive::CreateTypeSpec;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
mod permission;
|
||||||
|
pub use permission::{Permission, Permissions};
|
||||||
|
|
||||||
#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)]
|
#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)]
|
||||||
pub struct AssetId{
|
pub struct AssetId{
|
||||||
|
@ -19,54 +14,34 @@ pub struct AssetId{
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)]
|
#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)]
|
||||||
pub enum Permission {
|
pub struct NotamonId{
|
||||||
#[discriminant(0)]
|
id: u128,
|
||||||
None {},
|
|
||||||
#[discriminant(1)]
|
|
||||||
Addresses { addresses: Vec<Address> },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Permission {
|
#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)]
|
||||||
pub const NONE: Permission = Permission::None { };
|
pub struct SpeciesId{
|
||||||
|
id: u16,
|
||||||
pub fn only(address: Address) -> Permission {
|
|
||||||
Permission::Addresses { addresses: vec![address] }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn allows(&self, address: &Address) -> bool {
|
#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)]
|
||||||
match self {
|
pub struct SkinId{
|
||||||
Permission::None { } => false,
|
id: u8,
|
||||||
Permission::Addresses { addresses } => addresses.contains(address),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ReadWriteState, Debug, CreateTypeSpec)]
|
#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)]
|
||||||
pub struct Permissions<KeyT: ReadWriteState> {
|
pub struct EffectId{
|
||||||
permissions: AvlTreeMap<KeyT, Permission>,
|
id: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl <KeyT: ReadWriteState> Permissions<KeyT> {
|
|
||||||
|
|
||||||
pub fn new () -> Self {
|
type StatAmount = u8;
|
||||||
Self { permissions: AvlTreeMap::new() }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_permission(&mut self, permission_key: KeyT, permission: Permission) {
|
#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec)]
|
||||||
self.permissions.insert(permission_key, permission);
|
pub struct NotamonAttributes{
|
||||||
|
species_id: SpeciesId,
|
||||||
|
skin_id: SkinId,
|
||||||
|
effect_id: EffectId,
|
||||||
|
stat_hp: StatAmount,
|
||||||
|
stat_attack: StatAmount,
|
||||||
|
stat_defense: StatAmount,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_permission(&self, permission: &KeyT) -> Permission {
|
|
||||||
self.permissions.get(permission).unwrap_or(Permission::NONE)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
impl <KeyT: ReadWriteState + Debug> Permissions<KeyT> {
|
|
||||||
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:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
61
rust/notamon-common/src/permission.rs
Normal file
61
rust/notamon-common/src/permission.rs
Normal file
|
@ -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<Address> },
|
||||||
|
}
|
||||||
|
|
||||||
|
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<KeyT: ReadWriteState> {
|
||||||
|
permissions: AvlTreeMap<KeyT, Permission>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl <KeyT: ReadWriteState> Permissions<KeyT> {
|
||||||
|
|
||||||
|
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 <KeyT: ReadWriteState + Debug> Permissions<KeyT> {
|
||||||
|
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:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
27
rust/notamon-nft/Cargo.toml
Normal file
27
rust/notamon-nft/Cargo.toml
Normal file
|
@ -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"]
|
6
rust/notamon-nft/README.md
Normal file
6
rust/notamon-nft/README.md
Normal file
|
@ -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).
|
25
rust/notamon-nft/nft-v2/Cargo.toml
Normal file
25
rust/notamon-nft/nft-v2/Cargo.toml
Normal file
|
@ -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"]
|
42
rust/notamon-nft/nft-v2/README.md
Normal file
42
rust/notamon-nft/nft-v2/README.md
Normal file
|
@ -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.
|
382
rust/notamon-nft/nft-v2/src/lib.rs
Normal file
382
rust/notamon-nft/nft-v2/src/lib.rs
Normal file
|
@ -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<u128, Address>,
|
||||||
|
/// Mapping from token_id to the approved address who can transfer the token.
|
||||||
|
token_approvals: AvlTreeMap<u128, Address>,
|
||||||
|
/// Containing approved operators of owners. Operators can transfer and change approvals on all tokens owned by owner.
|
||||||
|
operator_approvals: AvlTreeMap<OperatorApproval, Unit>,
|
||||||
|
/// 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<u128, String>,
|
||||||
|
/// 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<Address>`] The approved address for this NFT, or none if there is none.
|
||||||
|
pub fn get_approved(&self, token_id: u128) -> 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`: [`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<Address>`], The new approved NFT controller.
|
||||||
|
///
|
||||||
|
/// * `token_id`: [`u128`], The NFT to approve.
|
||||||
|
pub fn _approve(&mut self, approved: Option<Address>, 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<Address>`], 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<Address>,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
411
rust/notamon-nft/src/lib.rs
Normal file
411
rust/notamon-nft/src/lib.rs
Normal file
|
@ -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<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, Unit>,
|
||||||
|
/// 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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
) -> 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<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, 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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user