diff --git a/rust/notamon-asset-contract/src/lib.rs b/rust/notamon-asset-contract/src/lib.rs index 1bb45d9..c9e3893 100644 --- a/rust/notamon-asset-contract/src/lib.rs +++ b/rust/notamon-asset-contract/src/lib.rs @@ -113,20 +113,25 @@ pub fn delete_asset( } #[action(shortname = 0x07, zk = true)] -pub fn publish_asset( +pub fn publish_assets( context: ContractContext, state: AssetContractState, zk_state: ZkState, - asset_id: AssetId, + asset_ids: Vec, ) -> (AssetContractState, Vec, Vec) { state.permissions.assert_has_permission(&context.sender, Role::PUBLISHER {}); - let variable_id = state.asset_id_to_variable_id.get(&asset_id).unwrap(); + let variable_ids: Vec = + asset_ids.into_iter().filter_map( + |asset_id| + state.asset_id_to_variable_id.get(&asset_id) + ).collect(); + ( state, vec![], vec![ZkStateChange::OpenVariables { - variables: vec![variable_id], + variables: variable_ids, }], ) } diff --git a/rust/notamon-common/src/lib.rs b/rust/notamon-common/src/lib.rs index 616590c..3f227b9 100644 --- a/rust/notamon-common/src/lib.rs +++ b/rust/notamon-common/src/lib.rs @@ -8,35 +8,60 @@ use std::fmt::Debug; mod permission; pub use permission::{Permission, Permissions}; -#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)] -pub struct AssetId{ - id: u32, -} - -#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)] +#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq, Clone)] pub struct NotamonId{ id: u128, } -#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)] +#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq, Clone)] +pub struct AssetId{ + id: u32, +} + +trait ToAssetId { + fn asset_id(&self) -> AssetId; +} + +#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq, Clone)] pub struct SpeciesId{ id: u16, } -#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)] +#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq, Clone)] pub struct SkinId{ id: u8, } -#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq)] +#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, PartialEq, Eq, Clone)] pub struct EffectId{ id: u8, } +const ASSET_ID_OFFSET_SPECIES: u32 = 0x0100_0000; +const ASSET_ID_OFFSET_SKIN: u32 = 0x0200_0000; +const ASSET_ID_OFFSET_EFFECT: u32 = 0x0300_0000; + +impl ToAssetId for SpeciesId { + fn asset_id(&self) -> AssetId { + AssetId { id: self.id as u32 | ASSET_ID_OFFSET_SPECIES } + } +} + +impl ToAssetId for SkinId { + fn asset_id(&self) -> AssetId { + AssetId { id: self.id as u32 | ASSET_ID_OFFSET_SKIN} + } +} + +impl ToAssetId for EffectId { + fn asset_id(&self) -> AssetId { + AssetId { id: self.id as u32 | ASSET_ID_OFFSET_EFFECT} + } +} type StatAmount = u8; -#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec)] +#[derive(ReadWriteState, ReadWriteRPC, Debug, CreateTypeSpec, Clone)] pub struct NotamonAttributes{ species_id: SpeciesId, skin_id: SkinId, @@ -45,3 +70,9 @@ pub struct NotamonAttributes{ stat_attack: StatAmount, stat_defense: StatAmount, } + +impl NotamonAttributes { + pub fn asset_ids(&self) -> [AssetId; 3] { + [self.species_id.asset_id(), self.skin_id.asset_id(), self.effect_id.asset_id()] + } +} diff --git a/rust/notamon-nft/nft-v2/Cargo.toml b/rust/notamon-nft/nft-v2/Cargo.toml deleted file mode 100644 index 330eba9..0000000 --- a/rust/notamon-nft/nft-v2/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[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 deleted file mode 100644 index ccd8047..0000000 --- a/rust/notamon-nft/nft-v2/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# 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 deleted file mode 100644 index ff32562..0000000 --- a/rust/notamon-nft/nft-v2/src/lib.rs +++ /dev/null @@ -1,382 +0,0 @@ -#![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 index 83da108..b14b612 100644 --- a/rust/notamon-nft/src/lib.rs +++ b/rust/notamon-nft/src/lib.rs @@ -4,13 +4,16 @@ #[macro_use] extern crate pbc_contract_codegen; +use pbc_contract_common::shortname::Shortname; + +use pbc_contract_common::events::EventGroup; use create_type_spec_derive::CreateTypeSpec; use pbc_contract_common::address::Address; -use pbc_contract_common::avl_tree_map::AvlTreeMap; +use pbc_contract_common::avl_tree_map::{AvlTreeMap, AvlTreeSet}; use pbc_contract_common::context::ContractContext; use read_write_state_derive::ReadWriteState; -use notamon_common::NotamonAttributes; +use notamon_common::{NotamonAttributes, AssetId}; /// A permission to transfer and approve NFTs given from an NFT owner to a separate address, called an operator. #[derive(ReadWriteState, CreateTypeSpec, PartialEq, Copy, Clone, Ord, PartialOrd, Eq)] @@ -70,6 +73,10 @@ pub struct NFTContractState { /// Notamon attributes notamon_attributes: AvlTreeMap, + + cached_asset_unlocks: AvlTreeSet, + + asset_contract: Address, } impl NFTContractState { @@ -224,6 +231,7 @@ pub fn initialize( token_uri_details: AvlTreeMap::new(), contract_owner: ctx.sender, notamon_attributes: AvlTreeMap::new(), + cached_asset_unlocks: AvlTreeSet::new(), } } @@ -367,14 +375,62 @@ pub fn mint( token_id: TokenId, token_uri: String, ) -> NFTContractState { + panic!("Default NFT minting is not supported") +} + +fn unlock_assets( + ctx: ContractContext, + mut state: NFTContractState, + notamon_attributes: NotamonAttributes, +) -> ( + NFTContractState, + Vec, +) { + let asset_ids: Vec = notamon_attributes.asset_ids().into_iter().filter( + |id| state.cached_asset_unlocks.contains(id) + ) .collect(); + + if asset_ids.is_empty() { + return (state, vec![]); + } + + for id in asset_ids.clone() { + state.cached_asset_unlocks.insert(id) + } + + let mut event_group_builder = EventGroup::builder(); + event_group_builder + .call(state.asset_contract, Shortname::from_u32(0x07)) + .argument(asset_ids) + .done(); + + ( + state, + vec![event_group_builder.build()] + ) +} + +#[action(shortname = 0x10)] +pub fn mint_notamon( + ctx: ContractContext, + mut state: NFTContractState, + to: Address, + token_id: TokenId, + notamon_attributes: NotamonAttributes, +) -> ( + NFTContractState, + Vec, +) { if ctx.sender != state.contract_owner { panic!("MPC-721: mint only callable by the contract owner") } else if state.exists(token_id) { panic!("MPC-721: token already minted") } else { + let token_uri: String = "TODO".into(); state.owners.insert(token_id, to); + state.notamon_attributes.insert(token_id, notamon_attributes.clone()); state.token_uri_details.insert(token_id, token_uri); - state + unlock_assets(ctx, state, notamon_attributes) } }