From a87e19d9ef6e328a21cfcf0c0772f4a7a6589b2b Mon Sep 17 00:00:00 2001 From: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> Date: Thu, 7 Mar 2024 23:21:27 +0000 Subject: [PATCH] feat(pop-api): further implementation of nfts api (#21) * fix: restore missing ink contract attribute * feat(pop-api): further implementation of nfts api * feat(pop-api): add support to query nft state in extension * feat(pop-api): continue support for nft state queries * fix: add missing import * fix: increase visibility * fix: add missing derives * feat(pop-api): add relay_chain_block_number function * refactor(nfts): group functions as per pallet * chore: address clippy warnings --------- Co-authored-by: Peter White --- Cargo.lock | 2 + contracts/pop-api-examples/nfts/lib.rs | 107 ++-- pop-api/Cargo.toml | 4 +- pop-api/primitives/Cargo.toml | 2 + pop-api/primitives/src/lib.rs | 15 + pop-api/primitives/src/storage_keys.rs | 23 + pop-api/src/lib.rs | 16 +- pop-api/src/v0/mod.rs | 9 + pop-api/src/v0/nfts.rs | 678 ++++++++++++++++++++++--- runtime/src/assets_config.rs | 4 + runtime/src/extensions.rs | 94 +++- 11 files changed, 819 insertions(+), 135 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3bb824fb..b139ee54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9033,6 +9033,7 @@ dependencies = [ name = "pop-api" version = "0.0.0" dependencies = [ + "enumflags2", "ink", "parity-scale-codec", "pop-api-primitives", @@ -9045,6 +9046,7 @@ dependencies = [ name = "pop-api-primitives" version = "0.0.0" dependencies = [ + "bounded-collections 0.1.9", "parity-scale-codec", "scale-info", ] diff --git a/contracts/pop-api-examples/nfts/lib.rs b/contracts/pop-api-examples/nfts/lib.rs index 77c5820a..02fbf986 100755 --- a/contracts/pop-api-examples/nfts/lib.rs +++ b/contracts/pop-api-examples/nfts/lib.rs @@ -5,63 +5,80 @@ use pop_api::nfts; #[derive(Debug, Copy, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub enum ContractError { - NftsError(nfts::Error), + InvalidCollection, + ItemAlreadyExists, + NftsError(nfts::Error), + NotOwner, } impl From for ContractError { - fn from(value: nfts::Error) -> Self { - ContractError::NftsError(value) - } + fn from(value: nfts::Error) -> Self { + ContractError::NftsError(value) + } } #[ink::contract(env = pop_api::Environment)] mod pop_api_extension_demo { - use super::ContractError; + use super::ContractError; - #[ink(storage)] - #[derive(Default)] - pub struct PopApiExtensionDemo; + #[ink(storage)] + #[derive(Default)] + pub struct PopApiExtensionDemo; - impl PopApiExtensionDemo { - #[ink(constructor, payable)] - pub fn new() -> Self { - ink::env::debug_println!("PopApiExtensionDemo::new"); - Default::default() - } + impl PopApiExtensionDemo { + #[ink(constructor, payable)] + pub fn new() -> Self { + ink::env::debug_println!("Contract::new"); + Default::default() + } - #[ink(message)] - pub fn mint_through_runtime( - &mut self, - collection_id: u32, - item_id: u32, - receiver: AccountId, - ) -> Result<(), ContractError> { - ink::env::debug_println!("PopApiExtensionDemo::mint_through_runtime: collection_id: {:?} \nitem_id {:?} \nreceiver: {:?}, ", collection_id, item_id, receiver); + #[ink(message)] + pub fn mint_through_runtime( + &mut self, + collection_id: u32, + item_id: u32, + receiver: AccountId, + ) -> Result<(), ContractError> { + ink::env::debug_println!( + "Contract::mint_through_runtime: collection_id: {:?} item_id {:?} receiver: {:?}", + collection_id, + item_id, + receiver + ); - // simplified API call - let result = pop_api::nfts::mint(collection_id, item_id, receiver); - ink::env::debug_println!( - "PopApiExtensionDemo::mint_through_runtime result: {result:?}" - ); - if let Err(pop_api::nfts::Error::NoConfig) = result { - ink::env::debug_println!( - "PopApiExtensionDemo::mint_through_runtime expected error received" - ); - } - result?; + // Check if item already exists (demo purposes only, unnecessary as would expect check in mint call) + if pop_api::nfts::item(collection_id, item_id)?.is_some() { + return Err(ContractError::ItemAlreadyExists); + } - ink::env::debug_println!("PopApiExtensionDemo::mint_through_runtime end"); - Ok(()) - } - } + // mint api + pop_api::nfts::mint(collection_id, item_id, receiver)?; + ink::env::debug_println!("Contract::mint_through_runtime: item minted successfully"); - #[cfg(test)] - mod tests { - use super::*; + // check owner + match pop_api::nfts::owner(collection_id, item_id)? { + Some(owner) if owner == receiver => { + ink::env::debug_println!( + "Contract::mint_through_runtime success: minted item belongs to receiver" + ); + }, + _ => { + return Err(ContractError::NotOwner); + }, + } - #[ink::test] - fn default_works() { - PopApiExtensionDemo::new(); - } - } + ink::env::debug_println!("Contract::mint_through_runtime end"); + Ok(()) + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[ink::test] + fn default_works() { + PopApiExtensionDemo::new(); + } + } } diff --git a/pop-api/Cargo.toml b/pop-api/Cargo.toml index 51d515c8..69b362f4 100644 --- a/pop-api/Cargo.toml +++ b/pop-api/Cargo.toml @@ -6,9 +6,10 @@ version = "0.0.0" edition = "2021" [dependencies] +enumflags2 = { version = "0.7.7" } ink = { version = "4.3.0", default-features = false } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } -scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } +scale-info = { version = "2.6", default-features = false, features = ["derive"] } sp-io = { version = "23.0.0", default-features = false, features = ["disable_panic_handler", "disable_oom", "disable_allocator"] } sp-runtime = { version = "24.0", default-features = false } @@ -22,6 +23,7 @@ crate-type = ["rlib"] [features] default = ["std"] std = [ + "enumflags2/std", "ink/std", "pop-api-primitives/std", "scale/std", diff --git a/pop-api/primitives/Cargo.toml b/pop-api/primitives/Cargo.toml index 80f0d929..ba8c6efd 100644 --- a/pop-api/primitives/Cargo.toml +++ b/pop-api/primitives/Cargo.toml @@ -6,6 +6,7 @@ version = "0.0.0" edition = "2021" [dependencies] +bounded-collections = { version = "0.1", default-features = false } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } @@ -17,6 +18,7 @@ crate-type = ["rlib"] [features] default = ["std"] std = [ + "bounded-collections/std", "scale/std", "scale-info/std", ] diff --git a/pop-api/primitives/src/lib.rs b/pop-api/primitives/src/lib.rs index b401dc5f..b3d84120 100644 --- a/pop-api/primitives/src/lib.rs +++ b/pop-api/primitives/src/lib.rs @@ -1,3 +1,18 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] +pub use bounded_collections::{BoundedBTreeMap, BoundedBTreeSet, BoundedVec, ConstU32}; +//use scale::{Decode, Encode, MaxEncodedLen}; + pub mod storage_keys; + +// /// Some way of identifying an account on the chain. +// #[derive(Encode, Decode, Debug, MaxEncodedLen)] +// pub struct AccountId([u8; 32]); +// Id used for identifying non-fungible collections. +pub type CollectionId = u32; +// Id used for identifying non-fungible items. +pub type ItemId = u32; +/// The maximum length of an attribute key. +pub type KeyLimit = ConstU32<64>; +/// The maximum approvals an item could have. +pub type ApprovalsLimit = ConstU32<20>; diff --git a/pop-api/primitives/src/storage_keys.rs b/pop-api/primitives/src/storage_keys.rs index 8b107eec..dbb2b3c0 100644 --- a/pop-api/primitives/src/storage_keys.rs +++ b/pop-api/primitives/src/storage_keys.rs @@ -1,7 +1,9 @@ +use super::*; use scale::{Decode, Encode, MaxEncodedLen}; #[derive(Encode, Decode, Debug, MaxEncodedLen)] pub enum RuntimeStateKeys { + Nfts(NftsKeys), ParachainSystem(ParachainSystemKeys), } @@ -9,3 +11,24 @@ pub enum RuntimeStateKeys { pub enum ParachainSystemKeys { LastRelayChainBlockNumber, } + +// https://github.com/paritytech/polkadot-sdk/blob/master/substrate/frame/nfts/src/impl_nonfungibles.rs +#[derive(Encode, Decode, Debug, MaxEncodedLen)] +pub enum NftsKeys { + // Get the details of a collection. + Collection(CollectionId), + /// Get the owner of the collection, if the collection exists. + CollectionOwner(CollectionId), + // Get the details of an item. + Item(CollectionId, ItemId), + /// Get the owner of the item, if the item exists. + Owner(CollectionId, ItemId), + /// Get the attribute value of `item` of `collection` corresponding to `key`. + Attribute(CollectionId, ItemId, BoundedVec), + // /// Get the custom attribute value of `item` of `collection` corresponding to `key`. + // CustomAttribute(AccountId, CollectionId, ItemId, BoundedVec), + /// Get the system attribute value of `item` of `collection` corresponding to `key` + SystemAttribute(CollectionId, Option, BoundedVec), + /// Get the attribute value of `item` of `collection` corresponding to `key`. + CollectionAttribute(CollectionId, BoundedVec), +} diff --git a/pop-api/src/lib.rs b/pop-api/src/lib.rs index af96523f..b33b6662 100644 --- a/pop-api/src/lib.rs +++ b/pop-api/src/lib.rs @@ -6,22 +6,14 @@ pub mod v0; use crate::PopApiError::{Balances, Nfts, UnknownStatusCode}; use ink::{prelude::vec::Vec, ChainExtensionInstance}; use primitives::storage_keys::*; -use scale; pub use sp_runtime::{BoundedVec, MultiAddress, MultiSignature}; use v0::RuntimeCall; -pub use v0::{balances, nfts, state}; +pub use v0::{balances, nfts, relay_chain_block_number, state}; -// Id used for identifying non-fungible collections. -pub type CollectionId = u32; - -// Id used for identifying non-fungible items. -pub type ItemId = u32; - -type AccountId = ::AccountId; -type Balance = ::Balance; -type BlockNumber = ::BlockNumber; +type AccountId = ::AccountId; +type Balance = ::Balance; +type BlockNumber = ::BlockNumber; type StringLimit = u32; -type KeyLimit = u32; type MaxTips = u32; pub type Result = core::result::Result; diff --git a/pop-api/src/v0/mod.rs b/pop-api/src/v0/mod.rs index 7b87dc25..a7d4e4b5 100644 --- a/pop-api/src/v0/mod.rs +++ b/pop-api/src/v0/mod.rs @@ -1,7 +1,16 @@ +use crate::{ + primitives::storage_keys::{ParachainSystemKeys, RuntimeStateKeys}, + BlockNumber, PopApiError, +}; + pub mod balances; pub mod nfts; pub mod state; +pub fn relay_chain_block_number() -> Result { + state::read(RuntimeStateKeys::ParachainSystem(ParachainSystemKeys::LastRelayChainBlockNumber)) +} + #[derive(scale::Encode)] pub(crate) enum RuntimeCall { #[codec(index = 10)] diff --git a/pop-api/src/v0/nfts.rs b/pop-api/src/v0/nfts.rs index 90e5cac0..f4e0d4e4 100644 --- a/pop-api/src/v0/nfts.rs +++ b/pop-api/src/v0/nfts.rs @@ -1,10 +1,27 @@ use super::RuntimeCall; use crate::{PopApiError::UnknownStatusCode, *}; use ink::prelude::vec::Vec; -use primitives::{BoundedVec, MultiAddress}; +use primitives::{ApprovalsLimit, BoundedBTreeMap, KeyLimit, MultiAddress}; +pub use primitives::{CollectionId, ItemId}; +use scale::Encode; +pub use types::*; type Result = core::result::Result; +/// Issue a new collection of non-fungible items +pub fn create( + admin: impl Into>, + config: CollectionConfig, +) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::Create { admin: admin.into(), config }))?) +} + +/// Destroy a collection of fungible items. +pub fn destroy(collection: CollectionId) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::Destroy { collection }))?) +} + +/// Mint an item of a particular collection. pub fn mint( collection: CollectionId, item: ItemId, @@ -18,14 +35,370 @@ pub fn mint( }))?) } -#[derive(scale::Encode)] -#[allow(dead_code)] +/// Destroy a single item. +pub fn burn(collection: CollectionId, item: ItemId) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::Burn { collection, item }))?) +} + +/// Move an item from the sender account to another. +pub fn transfer( + collection: CollectionId, + item: ItemId, + dest: impl Into>, +) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::Transfer { collection, item, dest: dest.into() }))?) +} + +/// Re-evaluate the deposits on some items. +pub fn redeposit(collection: CollectionId, items: Vec) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::Redeposit { collection, items }))?) +} + +/// Change the Owner of a collection. +pub fn transfer_ownership( + collection: CollectionId, + new_owner: impl Into>, +) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::TransferOwnership { + collection, + new_owner: new_owner.into(), + }))?) +} + +/// Set (or reset) the acceptance of ownership for a particular account. +pub fn set_accept_ownership( + collection: CollectionId, + maybe_collection: Option, +) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::SetAcceptOwnership { collection, maybe_collection }))?) +} + +/// Set the maximum number of items a collection could have. +pub fn set_collection_max_supply(collection: CollectionId, max_supply: u32) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::SetCollectionMaxSupply { collection, max_supply }))?) +} + +/// Update mint settings. +pub fn update_mint_settings(collection: CollectionId, mint_settings: MintSettings) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::UpdateMintSettings { collection, mint_settings }))?) +} + +/// Get the owner of the item, if the item exists. +pub fn owner(collection: CollectionId, item: ItemId) -> Result> { + Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::Owner(collection, item)))?) +} + +/// Get the owner of the collection, if the collection exists. +pub fn collection_owner(collection: CollectionId) -> Result> { + Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::CollectionOwner(collection)))?) +} + +/// Get the details of a collection. +pub fn collection(collection: CollectionId) -> Result> { + Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::Collection(collection)))?) +} + +/// Get the details of an item. +pub fn item(collection: CollectionId, item: ItemId) -> Result> { + Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::Item(collection, item)))?) +} + +pub mod approvals { + use super::*; + + /// Approve an item to be transferred by a delegated third-party account. + pub fn approve_transfer( + collection: CollectionId, + item: ItemId, + delegate: impl Into>, + maybe_deadline: Option, + ) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::ApproveTransfer { + collection, + item, + delegate: delegate.into(), + maybe_deadline, + }))?) + } + + /// Cancel one of the transfer approvals for a specific item. + pub fn cancel_approval( + collection: CollectionId, + item: ItemId, + delegate: impl Into>, + ) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::CancelApproval { + collection, + item, + delegate: delegate.into(), + }))?) + } + + /// Cancel all the approvals of a specific item. + pub fn clear_all_transfer_approvals(collection: CollectionId, item: ItemId) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::ClearAllTransferApprovals { collection, item }))?) + } +} + +pub mod attributes { + use super::*; + + /// Approve item's attributes to be changed by a delegated third-party account. + pub fn approve_item_attribute( + collection: CollectionId, + item: ItemId, + delegate: impl Into>, + ) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::ApproveItemAttribute { + collection, + item, + delegate: delegate.into(), + }))?) + } + + /// Cancel the previously provided approval to change item's attributes. + pub fn cancel_item_attributes_approval( + collection: CollectionId, + item: ItemId, + delegate: impl Into>, + ) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::CancelItemAttributesApproval { + collection, + item, + delegate: delegate.into(), + }))?) + } + + /// Set an attribute for a collection or item. + pub fn set_attribute( + collection: CollectionId, + maybe_item: Option, + namespace: AttributeNamespace, + key: BoundedVec, + value: BoundedVec, + ) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::SetAttribute { + collection, + maybe_item, + namespace, + key, + value, + }))?) + } + + /// Clear an attribute for a collection or item. + pub fn clear_attribute( + collection: CollectionId, + maybe_item: Option, + namespace: AttributeNamespace, + key: BoundedVec, + ) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::ClearAttribute { + collection, + maybe_item, + namespace, + key, + }))?) + } + + /// Get the attribute value of `item` of `collection` corresponding to `key`. + pub fn attribute( + collection: CollectionId, + item: ItemId, + key: BoundedVec, + ) -> Result>> { + Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::Attribute(collection, item, key)))?) + } + + // /// Get the custom attribute value of `item` of `collection` corresponding to `key`. + // pub fn custom_attribute( + // account: AccountId, + // collection: CollectionId, + // item: ItemId, + // key: BoundedVec, + // ) -> Result>> { + // Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::CustomAttribute( + // account, collection, item, key, + // )))?) + // } + + /// Get the system attribute value of `item` of `collection` corresponding to `key` if + /// `item` is `Some`. Otherwise, returns the system attribute value of `collection` + /// corresponding to `key`. + pub fn system_attribute( + collection: CollectionId, + item: Option, + key: BoundedVec, + ) -> Result>> { + Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::SystemAttribute(collection, item, key)))?) + } + + /// Get the attribute value of `item` of `collection` corresponding to `key`. + pub fn collection_attribute( + collection: CollectionId, + key: BoundedVec, + ) -> Result>> { + Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::CollectionAttribute(collection, key)))?) + } +} + +pub mod locking { + use super::*; + + /// Disallows changing the metadata or attributes of the item. + pub fn lock_item_properties( + collection: CollectionId, + item: ItemId, + lock_metadata: bool, + lock_attributes: bool, + ) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::LockItemProperties { + collection, + item, + lock_metadata, + lock_attributes, + }))?) + } + + /// Disallow further unprivileged transfer of an item. + pub fn lock_item_transfer(collection: CollectionId, item: ItemId) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::LockItemTransfer { collection, item }))?) + } + + /// Re-allow unprivileged transfer of an item. + pub fn unlock_item_transfer(collection: CollectionId, item: ItemId) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::UnlockItemTransfer { collection, item }))?) + } + + /// Disallows specified settings for the whole collection. + pub fn lock_collection( + collection: CollectionId, + lock_settings: CollectionSettings, + ) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::LockCollection { collection, lock_settings }))?) + } +} + +pub mod metadata { + use super::*; + + /// Set the metadata for an item. + pub fn set_metadata( + collection: CollectionId, + item: ItemId, + data: BoundedVec, + ) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::SetMetadata { collection, item, data }))?) + } + + /// Clear the metadata for an item. + pub fn clear_metadata(collection: CollectionId, item: ItemId) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::ClearMetadata { collection, item }))?) + } + + /// Set the metadata for a collection. + pub fn set_collection_metadata( + collection: CollectionId, + data: BoundedVec, + ) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::SetCollectionMetadata { collection, data }))?) + } + + /// Clear the metadata for a collection. + pub fn clear_collection_metadata(collection: CollectionId) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::ClearCollectionMetadata { collection }))?) + } +} + +pub mod roles { + use super::*; + + /// Change the Issuer, Admin and Freezer of a collection. + pub fn set_team( + collection: CollectionId, + issuer: Option>>, + admin: Option>>, + freezer: Option>>, + ) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::SetTeam { + collection, + issuer: issuer.map(|i| i.into()), + admin: admin.map(|i| i.into()), + freezer: freezer.map(|i| i.into()), + }))?) + } +} + +pub mod trading { + use super::*; + + /// Allows to pay the tips. + pub fn pay_tips(tips: BoundedVec) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::PayTips { tips }))?) + } + + /// Set (or reset) the price for an item. + pub fn price(collection: CollectionId, item: ItemId, price: Option) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::Price { collection, item, price }))?) + } + + /// Allows to buy an item if it's up for sale. + pub fn buy_item(collection: CollectionId, item: ItemId, bid_price: Balance) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::BuyItem { collection, item, bid_price }))?) + } + + pub mod swaps { + use super::*; + + /// Register a new atomic swap, declaring an intention to send an `item` in exchange for + /// `desired_item` from origin to target on the current chain. + pub fn create_swap( + offered_collection: CollectionId, + offered_item: ItemId, + desired_collection: CollectionId, + maybe_desired_item: Option, + maybe_price: Option, + duration: BlockNumber, + ) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::CreateSwap { + offered_collection, + offered_item, + desired_collection, + maybe_desired_item, + maybe_price, + duration, + }))?) + } + + /// Cancel an atomic swap. + pub fn cancel_swap(offered_collection: CollectionId, offered_item: ItemId) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::CancelSwap { + offered_collection, + offered_item, + }))?) + } + + /// Claim an atomic swap. + pub fn claim_swap( + send_collection: CollectionId, + send_item: ItemId, + receive_collection: CollectionId, + receive_item: ItemId, + ) -> Result<()> { + Ok(dispatch(RuntimeCall::Nfts(NftCalls::ClaimSwap { + send_collection, + send_item, + receive_collection, + receive_item, + }))?) + } + } +} + +#[derive(Encode)] pub(crate) enum NftCalls { - // #[codec(index = 0)] - // Create { - // admin: MultiAddress, - // config: CollectionConfig - // }, + #[codec(index = 0)] + Create { admin: MultiAddress, config: CollectionConfig }, #[codec(index = 2)] Destroy { collection: CollectionId }, #[codec(index = 3)] @@ -45,11 +418,8 @@ pub(crate) enum NftCalls { LockItemTransfer { collection: CollectionId, item: ItemId }, #[codec(index = 9)] UnlockItemTransfer { collection: CollectionId, item: ItemId }, - // #[codec(index = 10)] - // LockCollection { - // collection: CollectionId, - // lock_settings: CollectionSettings, - // }, + #[codec(index = 10)] + LockCollection { collection: CollectionId, lock_settings: CollectionSettings }, #[codec(index = 11)] TransferOwnership { collection: CollectionId, new_owner: MultiAddress }, #[codec(index = 12)] @@ -77,21 +447,21 @@ pub(crate) enum NftCalls { lock_metadata: bool, lock_attributes: bool, }, - // #[codec(index = 19)] - // SetAttribute { - // collection: CollectionId, - // maybe_item: Option, - // namespace: AttributeNamespace, - // key: BoundedVec, - // value: BoundedVec, - // }, - // #[codec(index = 21)] - // ClearAttribute { - // collection: CollectionId, - // maybe_item: Option, - // namespace: AttributeNamespace, - // key: BoundedVec, - // }, + #[codec(index = 19)] + SetAttribute { + collection: CollectionId, + maybe_item: Option, + namespace: AttributeNamespace, + key: BoundedVec, + value: BoundedVec, + }, + #[codec(index = 21)] + ClearAttribute { + collection: CollectionId, + maybe_item: Option, + namespace: AttributeNamespace, + key: BoundedVec, + }, #[codec(index = 22)] ApproveItemAttribute { collection: CollectionId, @@ -116,28 +486,23 @@ pub(crate) enum NftCalls { SetAcceptOwnership { collection: CollectionId, maybe_collection: Option }, #[codec(index = 29)] SetCollectionMaxSupply { collection: CollectionId, max_supply: u32 }, - // #[codec(index = 30)] - // UpdateMintSettings { - // collection: CollectionId, - // mint_settings: MintSettings, - // }, + #[codec(index = 30)] + UpdateMintSettings { collection: CollectionId, mint_settings: MintSettings }, #[codec(index = 31)] Price { collection: CollectionId, item: ItemId, price: Option }, #[codec(index = 32)] BuyItem { collection: CollectionId, item: ItemId, bid_price: Balance }, - // #[codec(index = 33)] - // PayTips { - // tips: BoundedVec, MaxTips> - // }, - // #[codec(index = 34)] - // CreateSwap { - // offered_collection: CollectionId, - // offered_item: ItemId, - // desired_collection: CollectionId, - // maybe_desired_item: Option, - // maybe_price: Option>, - // duration: BlockNumber, - // }, + #[codec(index = 33)] + PayTips { tips: BoundedVec }, + #[codec(index = 34)] + CreateSwap { + offered_collection: CollectionId, + offered_item: ItemId, + desired_collection: CollectionId, + maybe_desired_item: Option, + maybe_price: Option, + duration: BlockNumber, + }, #[codec(index = 35)] CancelSwap { offered_collection: CollectionId, offered_item: ItemId }, #[codec(index = 36)] @@ -147,21 +512,9 @@ pub(crate) enum NftCalls { receive_collection: CollectionId, receive_item: ItemId, }, - // #[codec(index = 37)] - // MintPreSigned { - // mint_data: PreSignedMint, - // signature: OffchainSignature, - // signer: AccountId - // }, - // #[codec(index = 38)] - // SetAttributesPreSigned { - // data: PreSignedAttributes, - // signature: OffchainSignature, - // signer: AccountId, - // } } -#[derive(Debug, Copy, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, scale::Decode)] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub enum Error { /// The signing account has no permission to do the operation. @@ -320,3 +673,210 @@ impl From for Error { } } } + +// Local implementations of pallet-nfts types +mod types { + use super::*; + use crate::{ + primitives::{CollectionId, ItemId}, + Balance, BlockNumber, + }; + use enumflags2::{bitflags, BitFlags}; + use scale::{Decode, EncodeLike, MaxEncodedLen}; + use scale_info::{build::Fields, meta_type, prelude::vec, Path, Type, TypeInfo, TypeParameter}; + + /// Attribute namespaces for non-fungible tokens. + #[derive(Encode)] + pub enum AttributeNamespace { + /// An attribute was set by the pallet. + Pallet, + /// An attribute was set by collection's owner. + CollectionOwner, + /// An attribute was set by item's owner. + ItemOwner, + /// An attribute was set by pre-approved account. + Account(AccountId), + } + + /// Collection's configuration. + #[derive(Encode)] + pub struct CollectionConfig { + /// Collection's settings. + pub settings: CollectionSettings, + /// Collection's max supply. + pub max_supply: Option, + /// Default settings each item will get during the mint. + pub mint_settings: MintSettings, + } + + /// Information about a collection. + #[derive(Decode, Debug, Encode, Eq, PartialEq)] + pub struct CollectionDetails { + /// Collection's owner. + pub owner: AccountId, + /// The total balance deposited by the owner for all the storage data associated with this + /// collection. Used by `destroy`. + pub owner_deposit: Balance, + /// The total number of outstanding items of this collection. + pub items: u32, + /// The total number of outstanding item metadata of this collection. + pub item_metadatas: u32, + /// The total number of outstanding item configs of this collection. + pub item_configs: u32, + /// The total number of attributes for this collection. + pub attributes: u32, + } + + /// Wrapper type for `BitFlags` that implements `Codec`. + pub struct CollectionSettings(pub BitFlags); + + impl_codec_bitflags!(CollectionSettings, u64, CollectionSetting); + + /// Support for up to 64 user-enabled features on a collection. + #[bitflags] + #[repr(u64)] + #[derive(Copy, Clone, Encode, TypeInfo)] + pub enum CollectionSetting { + /// Items in this collection are transferable. + TransferableItems, + /// The metadata of this collection can be modified. + UnlockedMetadata, + /// Attributes of this collection can be modified. + UnlockedAttributes, + /// The supply of this collection can be modified. + UnlockedMaxSupply, + /// When this isn't set then the deposit is required to hold the items of this collection. + DepositRequired, + } + + /// Information concerning the ownership of a single unique item. + #[derive(Decode, Debug, Encode, Eq, PartialEq)] + pub struct ItemDetails { + /// The owner of this item. + pub owner: AccountId, + /// The approved transferrer of this item, if one is set. + pub approvals: BoundedBTreeMap, ApprovalsLimit>, + /// The amount held in the pallet's default account for this item. Free-hold items will have + /// this as zero. + pub deposit: Balance, + } + + /// Support for up to 64 user-enabled features on an item. + #[bitflags] + #[repr(u64)] + #[derive(Copy, Clone, Encode, TypeInfo)] + pub enum ItemSetting { + /// This item is transferable. + Transferable, + /// The metadata of this item can be modified. + UnlockedMetadata, + /// Attributes of this item can be modified. + UnlockedAttributes, + } + + /// Wrapper type for `BitFlags` that implements `Codec`. + pub struct ItemSettings(pub BitFlags); + + impl_codec_bitflags!(ItemSettings, u64, ItemSetting); + + /// Information about the tip. + #[derive(Encode)] + pub struct ItemTip { + /// The collection of the item. + pub(super) collection: CollectionId, + /// An item of which the tip is sent for. + pub(super) item: ItemId, + /// A sender of the tip. + pub(super) receiver: AccountId, + /// An amount the sender is willing to tip. + pub(super) amount: Balance, + } + + /// Holds the information about minting. + #[derive(Encode)] + pub struct MintSettings { + /// Whether anyone can mint or if minters are restricted to some subset. + pub mint_type: MintType, + /// An optional price per mint. + pub price: Option, + /// When the mint starts. + pub start_block: Option, + /// When the mint ends. + pub end_block: Option, + /// Default settings each item will get during the mint. + pub default_item_settings: ItemSettings, + } + + /// Mint type. Can the NFT be create by anyone, or only the creator of the collection, + /// or only by wallets that already hold an NFT from a certain collection? + /// The ownership of a privately minted NFT is still publicly visible. + #[derive(Encode)] + pub enum MintType { + /// Only an `Issuer` could mint items. + Issuer, + /// Anyone could mint items. + Public, + /// Only holders of items in specified collection could mint new items. + HolderOf(CollectionId), + } + + /// Holds the details about the price. + #[derive(Encode)] + pub struct PriceWithDirection { + /// An amount. + pub(super) amount: Balance, + /// A direction (send or receive). + pub(super) direction: PriceDirection, + } + + /// Specifies whether the tokens will be sent or received. + #[derive(Encode)] + pub enum PriceDirection { + /// Tokens will be sent. + Send, + /// Tokens will be received. + Receive, + } + + macro_rules! impl_codec_bitflags { + ($wrapper:ty, $size:ty, $bitflag_enum:ty) => { + impl MaxEncodedLen for $wrapper { + fn max_encoded_len() -> usize { + <$size>::max_encoded_len() + } + } + impl Encode for $wrapper { + fn using_encoded R>(&self, f: F) -> R { + self.0.bits().using_encoded(f) + } + } + impl EncodeLike for $wrapper {} + impl Decode for $wrapper { + fn decode( + input: &mut I, + ) -> core::result::Result { + let field = <$size>::decode(input)?; + Ok(Self(BitFlags::from_bits(field as $size).map_err(|_| "invalid value")?)) + } + } + + impl TypeInfo for $wrapper { + type Identity = Self; + + fn type_info() -> Type { + Type::builder() + .path(Path::new("BitFlags", module_path!())) + .type_params(vec![TypeParameter::new( + "T", + Some(meta_type::<$bitflag_enum>()), + )]) + .composite( + Fields::unnamed() + .field(|f| f.ty::<$size>().type_name(stringify!($bitflag_enum))), + ) + } + } + }; + } + pub(crate) use impl_codec_bitflags; +} diff --git a/runtime/src/assets_config.rs b/runtime/src/assets_config.rs index e5aa144e..f2acffb0 100644 --- a/runtime/src/assets_config.rs +++ b/runtime/src/assets_config.rs @@ -38,7 +38,9 @@ parameter_types! { impl pallet_nfts::Config for Runtime { type RuntimeEvent = RuntimeEvent; + // TODO: source from primitives type CollectionId = CollectionId; + // TODO: source from primitives type ItemId = ItemId; type Currency = Balances; type CreateOrigin = AsEnsureOriginWithArg>; @@ -50,8 +52,10 @@ impl pallet_nfts::Config for Runtime { type AttributeDepositBase = NftsAttributeDepositBase; type DepositPerByte = NftsDepositPerByte; type StringLimit = ConstU32<256>; + // TODO: source from primitives type KeyLimit = ConstU32<64>; type ValueLimit = ConstU32<256>; + // TODO: source from primitives type ApprovalsLimit = ConstU32<20>; type ItemAttributesApprovalsLimit = ConstU32<30>; type MaxTips = ConstU32<10>; diff --git a/runtime/src/extensions.rs b/runtime/src/extensions.rs index 4b3eefbb..84f359c1 100644 --- a/runtime/src/extensions.rs +++ b/runtime/src/extensions.rs @@ -2,14 +2,18 @@ use cumulus_primitives_core::relay_chain::BlockNumber; use frame_support::{ dispatch::{GetDispatchInfo, PostDispatchInfo, RawOrigin}, pallet_prelude::*, + traits::nonfungibles_v2::Inspect, }; -use log; use pallet_contracts::chain_extension::{ - ChainExtension, Environment, Ext, InitState, RetVal, SysConfig, + BufInBufOutState, ChainExtension, Environment, Ext, InitState, RetVal, SysConfig, +}; +use pop_api_primitives::{ + storage_keys::{NftsKeys, ParachainSystemKeys, RuntimeStateKeys}, + CollectionId, ItemId, }; -use pop_api_primitives::storage_keys::ParachainSystemKeys; use sp_core::crypto::UncheckedFrom; use sp_runtime::{traits::Dispatchable, DispatchError}; +use sp_std::vec::Vec; const LOG_TARGET: &str = "pop-api::extension"; @@ -20,7 +24,10 @@ pub struct PopApiExtension; impl ChainExtension for PopApiExtension where - T: pallet_contracts::Config + cumulus_pallet_parachain_system::Config, + T: pallet_contracts::Config + + pallet_nfts::Config + + cumulus_pallet_parachain_system::Config + + frame_system::Config, ::AccountId: UncheckedFrom<::Hash> + AsRef<[u8]>, ::RuntimeCall: Parameter + Dispatchable::RuntimeOrigin, PostInfo = PostDispatchInfo> @@ -139,7 +146,10 @@ where fn read_state(env: Environment) -> Result<(), DispatchError> where - T: pallet_contracts::Config + cumulus_pallet_parachain_system::Config + frame_system::Config, + T: pallet_contracts::Config + + pallet_nfts::Config + + cumulus_pallet_parachain_system::Config + + frame_system::Config, E: Ext, { const LOG_PREFIX: &str = " read_state |"; @@ -155,23 +165,25 @@ where log::debug!(target:LOG_TARGET, "{} charged weight: {:?}", LOG_PREFIX, charged_weight); - let key: ParachainSystemKeys = env.read_as()?; + let key: RuntimeStateKeys = env.read_as()?; let result = match key { - ParachainSystemKeys::LastRelayChainBlockNumber => { - env.charge_weight(T::DbWeight::get().reads(1_u64))?; - let relay_block_num: BlockNumber = - cumulus_pallet_parachain_system::Pallet::::last_relay_block_number(); - log::debug!( - target:LOG_TARGET, - "{} last relay chain block number is: {:?}.", LOG_PREFIX, relay_block_num - ); - relay_block_num + RuntimeStateKeys::Nfts(key) => read_nfts_state::(key, &mut env), + RuntimeStateKeys::ParachainSystem(key) => match key { + ParachainSystemKeys::LastRelayChainBlockNumber => { + env.charge_weight(T::DbWeight::get().reads(1_u64))?; + let relay_block_num: BlockNumber = + cumulus_pallet_parachain_system::Pallet::::last_relay_block_number(); + log::debug!( + target:LOG_TARGET, + "{} last relay chain block number is: {:?}.", LOG_PREFIX, relay_block_num + ); + Ok(relay_block_num.encode()) + }, }, - } - .encode() - // Double-encode result for extension return type of bytes + }? .encode(); + log::trace!( target:LOG_TARGET, "{} result: {:?}.", LOG_PREFIX, result @@ -182,6 +194,52 @@ where }) } +fn read_nfts_state( + key: NftsKeys, + env: &mut Environment, +) -> Result, DispatchError> +where + T: pallet_contracts::Config + pallet_nfts::Config, + E: Ext, +{ + match key { + NftsKeys::Collection(collection) => { + env.charge_weight(T::DbWeight::get().reads(1_u64))?; + Ok(pallet_nfts::Collection::::get(collection).encode()) + }, + NftsKeys::CollectionOwner(collection) => { + env.charge_weight(T::DbWeight::get().reads(1_u64))?; + Ok(pallet_nfts::Pallet::::collection_owner(collection).encode()) + }, + NftsKeys::Item(collection, item) => { + env.charge_weight(T::DbWeight::get().reads(1_u64))?; + Ok(pallet_nfts::Item::::get(collection, item).encode()) + }, + NftsKeys::Owner(collection, item) => { + env.charge_weight(T::DbWeight::get().reads(1_u64))?; + Ok(pallet_nfts::Pallet::::owner(collection, item).encode()) + }, + NftsKeys::Attribute(collection, item, key) => { + env.charge_weight(T::DbWeight::get().reads(1_u64))?; + Ok(pallet_nfts::Pallet::::attribute(&collection, &item, &key).encode()) + }, + // NftsKeys::CustomAttribute(account, collection, item, key) => { + // env.charge_weight(T::DbWeight::get().reads(1_u64))?; + // Ok(pallet_nfts::Pallet::::custom_attribute(&account, &collection, &item, &key) + // .encode()) + // }, + NftsKeys::SystemAttribute(collection, item, key) => { + env.charge_weight(T::DbWeight::get().reads(1_u64))?; + Ok(pallet_nfts::Pallet::::system_attribute(&collection, item.as_ref(), &key) + .encode()) + }, + NftsKeys::CollectionAttribute(collection, key) => { + env.charge_weight(T::DbWeight::get().reads(1_u64))?; + Ok(pallet_nfts::Pallet::::collection_attribute(&collection, &key).encode()) + }, + } +} + #[cfg(test)] mod tests { pub use super::*;