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
+ }
+}