difftreelog
refactor split nonfungible into its own pallet
in: master
8 files changed
pallets/nonfungible/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/pallets/nonfungible/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "pallet-nonfungible"
+version = "0.1.0"
+edition = "2018"
+
+[dependencies.codec]
+default-features = false
+features = ['derive']
+package = 'parity-scale-codec'
+version = '2.0.0'
+
+[dependencies]
+frame-support = { default-features = false, version = '4.0.0-dev', git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.10' }
+frame-system = { default-features = false, version = '4.0.0-dev', git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.10' }
+sp-runtime = { default-features = false, version = '4.0.0-dev', git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.10' }
+sp-std = { default-features = false, version = '4.0.0-dev', git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.10' }
+sp-core = { default-features = false, version = '4.0.0-dev', git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.10' }
+pallet-common = { default-features = false, path = '../common' }
+nft-data-structs = { default-features = false, path = '../../primitives/nft' }
+evm-coder = { default-features = false, path = '../../crates/evm-coder' }
+pallet-evm-coder-substrate = { default-features = false, path = '../evm-coder-substrate' }
+ethereum = { default-features = false, version = "0.9.0" }
+
+[features]
+default = ["std"]
+std = [
+ "frame-support/std",
+ "frame-system/std",
+ "sp-runtime/std",
+ "sp-std/std",
+ "nft-data-structs/std",
+ "pallet-common/std",
+ "evm-coder/std",
+ "ethereum/std",
+ "pallet-evm-coder-substrate/std",
+]
+runtime-benchmarks = []
pallets/nonfungible/src/benchmarking.rsdiffbeforeafterboth--- /dev/null
+++ b/pallets/nonfungible/src/benchmarking.rs
@@ -0,0 +1 @@
+#![cfg(feature = "runtime-benchmarking")]
pallets/nonfungible/src/common.rsdiffbeforeafterboth--- /dev/null
+++ b/pallets/nonfungible/src/common.rs
@@ -0,0 +1,228 @@
+use core::marker::PhantomData;
+
+use frame_support::{dispatch::DispatchResultWithPostInfo, ensure, fail, weights::Weight};
+use nft_data_structs::TokenId;
+use pallet_common::{
+ CommonCollectionOperations, CommonWeightInfo, account::CrossAccountId, with_weight,
+};
+use sp_runtime::DispatchError;
+use sp_std::vec::Vec;
+
+use crate::{
+ AccountBalance, Allowance, Config, CreateItemData, DataKind, Error, NonfungibleHandle, Owned,
+ Owner, Pallet, SelfWeightOf, TokenData, weights::WeightInfo,
+};
+
+pub struct CommonWeights<T: Config>(PhantomData<T>);
+impl<T: Config> CommonWeightInfo for CommonWeights<T> {
+ fn create_item() -> Weight {
+ <SelfWeightOf<T>>::create_item()
+ }
+
+ fn create_multiple_items(amount: u32) -> Weight {
+ <SelfWeightOf<T>>::create_multiple_items(amount)
+ }
+
+ fn burn_item() -> Weight {
+ <SelfWeightOf<T>>::burn_item()
+ }
+
+ fn transfer() -> Weight {
+ <SelfWeightOf<T>>::transfer()
+ }
+
+ fn approve() -> Weight {
+ <SelfWeightOf<T>>::approve()
+ }
+
+ fn transfer_from() -> Weight {
+ <SelfWeightOf<T>>::transfer_from()
+ }
+
+ fn set_variable_metadata(_bytes: u32) -> Weight {
+ <SelfWeightOf<T>>::set_variable_metadata()
+ }
+}
+
+fn map_create_data<T: Config>(
+ data: nft_data_structs::CreateItemData,
+ to: &T::CrossAccountId,
+) -> Result<CreateItemData<T>, DispatchError> {
+ match data {
+ nft_data_structs::CreateItemData::NFT(data) => Ok(CreateItemData {
+ const_data: data.const_data,
+ variable_data: data.variable_data,
+ owner: to.clone(),
+ }),
+ _ => fail!(<Error<T>>::NotNonfungibleDataUsedToMintFungibleCollectionToken),
+ }
+}
+
+impl<T: Config> CommonCollectionOperations<T> for NonfungibleHandle<T> {
+ fn create_item(
+ &self,
+ sender: T::CrossAccountId,
+ to: T::CrossAccountId,
+ data: nft_data_structs::CreateItemData,
+ ) -> DispatchResultWithPostInfo {
+ with_weight(
+ <Pallet<T>>::create_item(self, &sender, map_create_data(data, &to)?),
+ <SelfWeightOf<T>>::create_item(),
+ )
+ }
+
+ fn create_multiple_items(
+ &self,
+ sender: T::CrossAccountId,
+ to: T::CrossAccountId,
+ data: Vec<nft_data_structs::CreateItemData>,
+ ) -> DispatchResultWithPostInfo {
+ let data = data
+ .into_iter()
+ .map(|d| map_create_data::<T>(d, &to))
+ .collect::<Result<Vec<_>, DispatchError>>()?;
+
+ let amount = data.len();
+ with_weight(
+ <Pallet<T>>::create_multiple_items(self, &sender, data),
+ <SelfWeightOf<T>>::create_multiple_items(amount as u32),
+ )
+ }
+
+ fn burn_item(
+ &self,
+ sender: T::CrossAccountId,
+ token: TokenId,
+ amount: u128,
+ ) -> DispatchResultWithPostInfo {
+ ensure!(amount <= 1, <Error<T>>::NonfungibleItemsHaveNoAmount);
+ if amount == 1 {
+ with_weight(
+ <Pallet<T>>::burn(&self, &sender, token),
+ <SelfWeightOf<T>>::burn_item(),
+ )
+ } else {
+ Ok(().into())
+ }
+ }
+
+ fn transfer(
+ &self,
+ from: T::CrossAccountId,
+ to: T::CrossAccountId,
+ token: TokenId,
+ amount: u128,
+ ) -> DispatchResultWithPostInfo {
+ ensure!(amount <= 1, <Error<T>>::NonfungibleItemsHaveNoAmount);
+ if amount == 1 {
+ with_weight(
+ <Pallet<T>>::transfer(&self, &from, &to, token),
+ <SelfWeightOf<T>>::transfer(),
+ )
+ } else {
+ Ok(().into())
+ }
+ }
+
+ fn approve(
+ &self,
+ sender: T::CrossAccountId,
+ spender: T::CrossAccountId,
+ token: TokenId,
+ amount: u128,
+ ) -> DispatchResultWithPostInfo {
+ ensure!(amount <= 1, <Error<T>>::NonfungibleItemsHaveNoAmount);
+
+ with_weight(
+ if amount == 1 {
+ <Pallet<T>>::set_allowance(&self, &sender, token, Some(&spender))
+ } else {
+ <Pallet<T>>::set_allowance(&self, &sender, token, None)
+ },
+ <SelfWeightOf<T>>::approve(),
+ )
+ }
+
+ fn transfer_from(
+ &self,
+ sender: T::CrossAccountId,
+ from: T::CrossAccountId,
+ to: T::CrossAccountId,
+ token: TokenId,
+ amount: u128,
+ ) -> DispatchResultWithPostInfo {
+ ensure!(amount <= 1, <Error<T>>::NonfungibleItemsHaveNoAmount);
+
+ if amount == 1 {
+ with_weight(
+ <Pallet<T>>::transfer_from(&self, &sender, &from, &to, token),
+ <SelfWeightOf<T>>::transfer_from(),
+ )
+ } else {
+ Ok(().into())
+ }
+ }
+
+ fn set_variable_metadata(
+ &self,
+ sender: T::CrossAccountId,
+ token: TokenId,
+ data: Vec<u8>,
+ ) -> DispatchResultWithPostInfo {
+ with_weight(
+ <Pallet<T>>::set_variable_metadata(&self, &sender, token, data),
+ <SelfWeightOf<T>>::set_variable_metadata(),
+ )
+ }
+
+ fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId> {
+ <Owned<T>>::iter_prefix((self.id, account.as_sub()))
+ .map(|(id, _)| id)
+ .collect()
+ }
+
+ fn token_exists(&self, token: TokenId) -> bool {
+ <Pallet<T>>::token_exists(self, token)
+ }
+
+ fn token_owner(&self, token: TokenId) -> T::CrossAccountId {
+ <Owner<T>>::get((self.id, token))
+ }
+ fn const_metadata(&self, token: TokenId) -> Vec<u8> {
+ <TokenData<T>>::get((self.id, token, DataKind::Constant))
+ }
+ fn variable_metadata(&self, token: TokenId) -> Vec<u8> {
+ <TokenData<T>>::get((self.id, token, DataKind::Variable))
+ }
+
+ fn collection_tokens(&self) -> u32 {
+ <Pallet<T>>::total_supply(self)
+ }
+
+ fn account_balance(&self, account: T::CrossAccountId) -> u32 {
+ <AccountBalance<T>>::get((self.id, account.as_sub()))
+ }
+
+ fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128 {
+ if <Owner<T>>::get((self.id, token)) == account {
+ 1
+ } else {
+ 0
+ }
+ }
+
+ fn allowance(
+ &self,
+ sender: T::CrossAccountId,
+ spender: T::CrossAccountId,
+ token: TokenId,
+ ) -> u128 {
+ if <Owner<T>>::get((self.id, token)) != sender {
+ 0
+ } else if <Allowance<T>>::get((self.id, token)) == Some(spender) {
+ 1
+ } else {
+ 0
+ }
+ }
+}
pallets/nonfungible/src/erc.rsdiffbeforeafterboth--- /dev/null
+++ b/pallets/nonfungible/src/erc.rs
@@ -0,0 +1,396 @@
+use core::{
+ char::{REPLACEMENT_CHARACTER, decode_utf16},
+ convert::TryInto,
+};
+use evm_coder::{ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*};
+use frame_support::BoundedVec;
+use nft_data_structs::TokenId;
+use pallet_evm_coder_substrate::dispatch_to_evm;
+use sp_core::{H160, U256};
+use sp_std::{vec::Vec, vec};
+use pallet_common::{account::CrossAccountId, erc::CommonEvmHandler};
+use pallet_evm_coder_substrate::call_internal;
+use pallet_common::erc::PrecompileOutput;
+
+use crate::{
+ AccountBalance, Config, CreateItemData, DataKind, NonfungibleHandle, Owner, Pallet, TokenData,
+ TokensMinted,
+};
+
+#[derive(ToLog)]
+pub enum ERC721Events {
+ Transfer {
+ #[indexed]
+ from: address,
+ #[indexed]
+ to: address,
+ #[indexed]
+ token_id: uint256,
+ },
+ Approval {
+ #[indexed]
+ owner: address,
+ #[indexed]
+ approved: address,
+ #[indexed]
+ token_id: uint256,
+ },
+ #[allow(dead_code)]
+ ApprovalForAll {
+ #[indexed]
+ owner: address,
+ #[indexed]
+ operator: address,
+ approved: bool,
+ },
+}
+
+#[derive(ToLog)]
+pub enum ERC721MintableEvents {
+ #[allow(dead_code)]
+ MintingFinished {},
+}
+
+#[solidity_interface(name = "ERC721Metadata")]
+impl<T: Config> NonfungibleHandle<T> {
+ fn name(&self) -> Result<string> {
+ Ok(decode_utf16(self.name.iter().copied())
+ .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))
+ .collect::<string>())
+ }
+ fn symbol(&self) -> Result<string> {
+ Ok(string::from_utf8_lossy(&self.token_prefix).into())
+ }
+
+ #[solidity(rename_selector = "tokenURI")]
+ fn token_uri(&self, token_id: uint256) -> Result<string> {
+ let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
+ Ok(string::from_utf8_lossy(&<TokenData<T>>::get((
+ self.id,
+ token_id,
+ DataKind::Constant,
+ )))
+ .into())
+ }
+}
+
+#[solidity_interface(name = "ERC721Enumerable")]
+impl<T: Config> NonfungibleHandle<T> {
+ fn token_by_index(&self, index: uint256) -> Result<uint256> {
+ Ok(index)
+ }
+
+ fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {
+ // TODO: Not implemetable
+ Err("not implemented".into())
+ }
+
+ fn total_supply(&self) -> Result<uint256> {
+ Ok(<Pallet<T>>::total_supply(self).into())
+ }
+}
+
+#[solidity_interface(name = "ERC721", events(ERC721Events))]
+impl<T: Config> NonfungibleHandle<T> {
+ fn balance_of(&self, owner: address) -> Result<uint256> {
+ let owner = T::CrossAccountId::from_eth(owner);
+ let balance = <AccountBalance<T>>::get((self.id, owner.as_sub()));
+ Ok(balance.into())
+ }
+ fn owner_of(&self, token_id: uint256) -> Result<address> {
+ let token: TokenId = token_id.try_into()?;
+ Ok(*<Owner<T>>::get((self.id, token)).as_eth())
+ }
+ fn safe_transfer_from_with_data(
+ &mut self,
+ _from: address,
+ _to: address,
+ _token_id: uint256,
+ _data: bytes,
+ _value: value,
+ ) -> Result<void> {
+ // TODO: Not implemetable
+ Err("not implemented".into())
+ }
+ fn safe_transfer_from(
+ &mut self,
+ _from: address,
+ _to: address,
+ _token_id: uint256,
+ _value: value,
+ ) -> Result<void> {
+ // TODO: Not implemetable
+ Err("not implemented".into())
+ }
+
+ fn transfer_from(
+ &mut self,
+ caller: caller,
+ from: address,
+ to: address,
+ token_id: uint256,
+ _value: value,
+ ) -> Result<void> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let from = T::CrossAccountId::from_eth(from);
+ let to = T::CrossAccountId::from_eth(to);
+ let token = token_id.try_into()?;
+
+ <Pallet<T>>::transfer_from(self, &caller, &from, &to, token)
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(())
+ }
+
+ fn approve(
+ &mut self,
+ caller: caller,
+ approved: address,
+ token_id: uint256,
+ _value: value,
+ ) -> Result<void> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let approved = T::CrossAccountId::from_eth(approved);
+ let token = token_id.try_into()?;
+
+ <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(())
+ }
+
+ fn set_approval_for_all(
+ &mut self,
+ _caller: caller,
+ _operator: address,
+ _approved: bool,
+ ) -> Result<void> {
+ // TODO: Not implemetable
+ Err("not implemented".into())
+ }
+
+ fn get_approved(&self, _token_id: uint256) -> Result<address> {
+ // TODO: Not implemetable
+ Err("not implemented".into())
+ }
+
+ fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {
+ // TODO: Not implemetable
+ Err("not implemented".into())
+ }
+}
+
+#[solidity_interface(name = "ERC721Burnable")]
+impl<T: Config> NonfungibleHandle<T> {
+ fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let token = token_id.try_into()?;
+
+ <Pallet<T>>::burn(self, &caller, token).map_err(dispatch_to_evm::<T>)?;
+ Ok(())
+ }
+}
+
+#[solidity_interface(name = "ERC721Mintable", events(ERC721MintableEvents))]
+impl<T: Config> NonfungibleHandle<T> {
+ fn minting_finished(&self) -> Result<bool> {
+ Ok(false)
+ }
+
+ fn mint(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let to = T::CrossAccountId::from_eth(to);
+ let token_id: u32 = token_id.try_into()?;
+ if <TokensMinted<T>>::get(self.id)
+ .checked_add(1)
+ .ok_or("item id overflow")?
+ != token_id
+ {
+ return Err("item id should be next".into());
+ }
+
+ <Pallet<T>>::create_item(
+ self,
+ &caller,
+ CreateItemData {
+ const_data: BoundedVec::default(),
+ variable_data: BoundedVec::default(),
+ owner: to,
+ },
+ )
+ .map_err(dispatch_to_evm::<T>)?;
+
+ Ok(true)
+ }
+
+ #[solidity(rename_selector = "mintWithTokenURI")]
+ fn mint_with_token_uri(
+ &mut self,
+ caller: caller,
+ to: address,
+ token_id: uint256,
+ token_uri: string,
+ ) -> Result<bool> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let to = T::CrossAccountId::from_eth(to);
+ let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;
+ if <TokensMinted<T>>::get(self.id)
+ .checked_add(1)
+ .ok_or("item id overflow")?
+ != token_id
+ {
+ return Err("item id should be next".into());
+ }
+
+ <Pallet<T>>::create_item(
+ self,
+ &caller,
+ CreateItemData {
+ const_data: Vec::<u8>::from(token_uri)
+ .try_into()
+ .map_err(|_| "token uri is too long")?,
+ variable_data: BoundedVec::default(),
+ owner: to,
+ },
+ )
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(true)
+ }
+
+ fn finish_minting(&mut self, _caller: caller) -> Result<bool> {
+ Err("not implementable".into())
+ }
+}
+
+#[solidity_interface(name = "ERC721UniqueExtensions")]
+impl<T: Config> NonfungibleHandle<T> {
+ #[solidity(rename_selector = "transfer")]
+ fn transfer_nft(
+ &mut self,
+ caller: caller,
+ to: address,
+ token_id: uint256,
+ _value: value,
+ ) -> Result<void> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let to = T::CrossAccountId::from_eth(to);
+ let token = token_id.try_into()?;
+
+ <Pallet<T>>::transfer(self, &caller, &to, token).map_err(dispatch_to_evm::<T>)?;
+ Ok(())
+ }
+
+ fn next_token_id(&self) -> Result<uint256> {
+ Ok(<TokensMinted<T>>::get(self.id)
+ .checked_add(1)
+ .ok_or("item id overflow")?
+ .into())
+ }
+
+ fn set_variable_metadata(
+ &mut self,
+ caller: caller,
+ token_id: uint256,
+ data: bytes,
+ ) -> Result<void> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let token = token_id.try_into()?;
+
+ <Pallet<T>>::set_variable_metadata(self, &caller, token, data)
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(())
+ }
+
+ fn get_variable_metadata(&self, token_id: uint256) -> Result<bytes> {
+ let token: TokenId = token_id.try_into()?;
+
+ Ok(<TokenData<T>>::get((self.id, token, DataKind::Variable)))
+ }
+
+ fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let to = T::CrossAccountId::from_eth(to);
+ let mut expected_index = <TokensMinted<T>>::get(self.id)
+ .checked_add(1)
+ .ok_or("item id overflow")?;
+
+ let total_tokens = token_ids.len();
+ for id in token_ids.into_iter() {
+ let id: u32 = id.try_into().map_err(|_| "token id overflow")?;
+ if id != expected_index {
+ return Err("item id should be next".into());
+ }
+ expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;
+ }
+ let data = (0..total_tokens)
+ .map(|_| CreateItemData {
+ const_data: BoundedVec::default(),
+ variable_data: BoundedVec::default(),
+ owner: to.clone(),
+ })
+ .collect();
+
+ <Pallet<T>>::create_multiple_items(self, &caller, data).map_err(dispatch_to_evm::<T>)?;
+ Ok(true)
+ }
+
+ #[solidity(rename_selector = "mintBulkWithTokenURI")]
+ fn mint_bulk_with_token_uri(
+ &mut self,
+ caller: caller,
+ to: address,
+ tokens: Vec<(uint256, string)>,
+ ) -> Result<bool> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let to = T::CrossAccountId::from_eth(to);
+ let mut expected_index = <TokensMinted<T>>::get(self.id)
+ .checked_add(1)
+ .ok_or("item id overflow")?;
+
+ let mut data = Vec::with_capacity(tokens.len());
+ for (id, token_uri) in tokens {
+ let id: u32 = id.try_into().map_err(|_| "token id overflow")?;
+ if id != expected_index {
+ panic!("item id should be next ({}) but got {}", expected_index, id);
+ }
+ expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;
+
+ data.push(CreateItemData {
+ const_data: Vec::<u8>::from(token_uri)
+ .try_into()
+ .map_err(|_| "token uri is too long")?,
+ variable_data: vec![].try_into().unwrap(),
+ owner: to.clone(),
+ });
+ }
+
+ <Pallet<T>>::create_multiple_items(self, &caller, data).map_err(dispatch_to_evm::<T>)?;
+ Ok(true)
+ }
+}
+
+#[solidity_interface(
+ name = "UniqueNFT",
+ is(
+ ERC721,
+ ERC721Metadata,
+ ERC721Enumerable,
+ ERC721UniqueExtensions,
+ ERC721Mintable,
+ ERC721Burnable,
+ )
+)]
+impl<T: Config> NonfungibleHandle<T> {}
+
+// Not a tests, but code generators
+generate_stubgen!(gen_impl, UniqueNFTCall, true);
+generate_stubgen!(gen_iface, UniqueNFTCall, false);
+
+pub const CODE: &[u8] = include_bytes!("./stubs/UniqueNFT.raw");
+
+impl<T: Config> CommonEvmHandler for NonfungibleHandle<T> {
+ const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw");
+
+ fn call(mut self, source: &H160, input: &[u8], value: U256) -> Option<PrecompileOutput> {
+ let result = call_internal::<UniqueNFTCall, _>(*source, &mut self, value, input);
+ self.0.recorder.evm_to_precompile_output(result)
+ }
+}
pallets/nonfungible/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/pallets/nonfungible/src/lib.rs
@@ -0,0 +1,537 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use erc::ERC721Events;
+use frame_support::{BoundedVec, ensure};
+use nft_data_structs::{
+ AccessMode, CUSTOM_DATA_LIMIT, Collection, CollectionId, CustomDataLimit, TokenId,
+};
+use pallet_common::{
+ Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, account::CrossAccountId,
+};
+use sp_core::H160;
+use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};
+use sp_std::{vec::Vec, vec};
+use core::ops::Deref;
+use sp_std::collections::btree_map::BTreeMap;
+
+pub use pallet::*;
+pub mod benchmarking;
+pub mod common;
+pub mod erc;
+pub mod weights;
+
+pub struct CreateItemData<T: Config> {
+ pub const_data: BoundedVec<u8, CustomDataLimit>,
+ pub variable_data: BoundedVec<u8, CustomDataLimit>,
+ pub owner: T::CrossAccountId,
+}
+pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;
+
+#[frame_support::pallet]
+pub mod pallet {
+ use frame_support::{Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key};
+ use sp_std::vec::Vec;
+ use nft_data_structs::{CollectionId, TokenId};
+ use super::weights::WeightInfo;
+
+ #[pallet::error]
+ pub enum Error<T> {
+ /// Not Nonfungible item data used to mint in Nonfungible collection.
+ NotNonfungibleDataUsedToMintFungibleCollectionToken,
+ /// Used amount > 1 with NFT
+ NonfungibleItemsHaveNoAmount,
+ }
+
+ #[pallet::config]
+ pub trait Config: frame_system::Config + pallet_common::Config {
+ type WeightInfo: WeightInfo;
+ }
+
+ #[pallet::pallet]
+ #[pallet::generate_store(pub(super) trait Store)]
+ pub struct Pallet<T>(_);
+
+ #[pallet::storage]
+ pub(super) type TokensMinted<T: Config> =
+ StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;
+ #[pallet::storage]
+ pub(super) type TokensBurnt<T: Config> =
+ StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;
+
+ #[derive(Encode, Decode)]
+ pub enum DataKind {
+ Constant,
+ Variable,
+ }
+
+ #[pallet::storage]
+ pub(super) type TokenData<T: Config> = StorageNMap<
+ Key = (
+ Key<Twox64Concat, CollectionId>,
+ Key<Twox64Concat, TokenId>,
+ Key<Identity, DataKind>,
+ ),
+ Value = Vec<u8>,
+ QueryKind = ValueQuery,
+ >;
+
+ #[pallet::storage]
+ pub(super) type Owner<T: Config> = StorageNMap<
+ Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),
+ Value = T::CrossAccountId,
+ QueryKind = ValueQuery,
+ >;
+ /// Used to enumerate tokens owned by account
+ #[pallet::storage]
+ pub(super) type Owned<T: Config> = StorageNMap<
+ Key = (
+ Key<Twox64Concat, CollectionId>,
+ Key<Blake2_128Concat, T::AccountId>,
+ Key<Twox64Concat, TokenId>,
+ ),
+ Value = bool,
+ QueryKind = ValueQuery,
+ >;
+
+ #[pallet::storage]
+ pub(super) type AccountBalance<T: Config> = StorageNMap<
+ Key = (
+ Key<Twox64Concat, CollectionId>,
+ Key<Blake2_128Concat, T::AccountId>,
+ ),
+ Value = u32,
+ QueryKind = ValueQuery,
+ >;
+
+ #[pallet::storage]
+ pub(super) type Allowance<T: Config> = StorageNMap<
+ Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),
+ Value = T::CrossAccountId,
+ QueryKind = OptionQuery,
+ >;
+}
+
+pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);
+impl<T: Config> NonfungibleHandle<T> {
+ pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {
+ Self(inner)
+ }
+ pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {
+ self.0
+ }
+}
+impl<T: Config> Deref for NonfungibleHandle<T> {
+ type Target = pallet_common::CollectionHandle<T>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl<T: Config> Pallet<T> {
+ pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {
+ <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)
+ }
+ pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {
+ <Owner<T>>::contains_key((collection.id, token))
+ }
+ pub fn ensure_owner(
+ collection: &NonfungibleHandle<T>,
+ token: TokenId,
+ sender: &T::CrossAccountId,
+ ) -> DispatchResult {
+ ensure!(
+ &<Owner<T>>::get((collection.id, token)) == sender,
+ <CommonError<T>>::NoPermission
+ );
+ Ok(())
+ }
+ pub fn item_owner(
+ collection: &NonfungibleHandle<T>,
+ token: TokenId,
+ ) -> Result<T::CrossAccountId, DispatchError> {
+ let owner = <Owner<T>>::get((collection.id, token));
+ ensure!(
+ owner != T::CrossAccountId::default(),
+ <CommonError<T>>::TokenNotFound
+ );
+ Ok(owner)
+ }
+}
+
+// unchecked calls skips any permission checks
+impl<T: Config> Pallet<T> {
+ pub fn init_collection(data: Collection<T>) -> Result<CollectionId, DispatchError> {
+ PalletCommon::init_collection(data)
+ }
+ pub fn destroy_collection(
+ collection: NonfungibleHandle<T>,
+ sender: &T::CrossAccountId,
+ ) -> DispatchResult {
+ let id = collection.id;
+
+ // =========
+
+ PalletCommon::destroy_collection(collection.0, sender)?;
+
+ <Owner<T>>::remove_prefix((id,), None);
+ <Owned<T>>::remove_prefix((id,), None);
+ <TokensMinted<T>>::remove(id);
+ <TokensBurnt<T>>::remove(id);
+ <TokenData<T>>::remove_prefix((id,), None);
+ <Allowance<T>>::remove_prefix((id,), None);
+ <AccountBalance<T>>::remove_prefix((id,), None);
+ Ok(())
+ }
+
+ pub fn burn(
+ collection: &NonfungibleHandle<T>,
+ sender: &T::CrossAccountId,
+ token: TokenId,
+ ) -> DispatchResult {
+ let token_owner = <Pallet<T>>::item_owner(collection, token)?;
+ ensure!(
+ &token_owner == sender
+ || (collection.limits.owner_can_transfer
+ && collection.is_owner_or_admin(sender)?),
+ <CommonError<T>>::NoPermission
+ );
+
+ if collection.access == AccessMode::WhiteList {
+ collection.check_whitelist(sender)?;
+ }
+
+ let burnt = <TokensBurnt<T>>::get(collection.id)
+ .checked_add(1)
+ .ok_or(ArithmeticError::Overflow)?;
+
+ // =========
+
+ <Owner<T>>::remove((collection.id, token));
+ <Owned<T>>::remove((collection.id, token_owner.as_sub(), token));
+ <TokensBurnt<T>>::insert(collection.id, burnt);
+ <TokenData<T>>::remove_prefix((collection.id, token), None);
+ <Allowance<T>>::remove((collection.id, token));
+
+ collection.log_infallible(ERC721Events::Transfer {
+ from: *token_owner.as_eth(),
+ to: H160::default(),
+ token_id: token.into(),
+ });
+ <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(
+ collection.id,
+ token,
+ token_owner,
+ 1,
+ ));
+ return Ok(());
+ }
+
+ pub fn transfer(
+ collection: &NonfungibleHandle<T>,
+ from: &T::CrossAccountId,
+ to: &T::CrossAccountId,
+ token: TokenId,
+ ) -> DispatchResult {
+ ensure!(
+ collection.transfers_enabled,
+ <CommonError<T>>::TransferNotAllowed
+ );
+
+ let token_owner = <Pallet<T>>::item_owner(collection, token)?;
+ ensure!(
+ &token_owner == from
+ || (collection.limits.owner_can_transfer && collection.is_owner_or_admin(from)?),
+ <CommonError<T>>::NoPermission
+ );
+
+ if collection.access == AccessMode::WhiteList {
+ collection.check_whitelist(from)?;
+ collection.check_whitelist(to)?;
+ }
+ <PalletCommon<T>>::ensure_correct_receiver(to)?;
+
+ let balance_from = <AccountBalance<T>>::get((collection.id, from.as_sub()))
+ .checked_sub(1)
+ .ok_or(<CommonError<T>>::TokenValueTooLow)?;
+ let balance_to = if from != to {
+ let balance_to = <AccountBalance<T>>::get((collection.id, to.as_sub()))
+ .checked_add(1)
+ .ok_or(ArithmeticError::Overflow)?;
+
+ ensure!(
+ balance_to < collection.limits.account_token_ownership_limit(),
+ <CommonError<T>>::AccountTokenLimitExceeded,
+ );
+
+ Some(balance_to)
+ } else {
+ None
+ };
+
+ collection.consume_sstores(4)?;
+ collection.consume_log(3, 0)?;
+
+ // =========
+
+ if let Some(balance_to) = balance_to {
+ // from != to
+ if balance_from == 0 {
+ <AccountBalance<T>>::remove((collection.id, from.as_sub()));
+ } else {
+ <AccountBalance<T>>::insert((collection.id, from.as_sub()), balance_from);
+ }
+ <AccountBalance<T>>::insert((collection.id, to.as_sub()), balance_to);
+ <Owned<T>>::remove((collection.id, from.as_sub(), token));
+ <Owned<T>>::insert((collection.id, to.as_sub(), token), true);
+ }
+ Self::set_allowance_unchecked(collection, from, token, None);
+ <Owner<T>>::insert((collection.id, token), &to);
+
+ collection.log_infallible(ERC721Events::Transfer {
+ from: *from.as_eth(),
+ to: *to.as_eth(),
+ token_id: token.into(),
+ });
+ <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(
+ collection.id,
+ token,
+ from.clone(),
+ to.clone(),
+ 1,
+ ));
+ Ok(())
+ }
+
+ pub fn create_multiple_items(
+ collection: &NonfungibleHandle<T>,
+ sender: &T::CrossAccountId,
+ data: Vec<CreateItemData<T>>,
+ ) -> DispatchResult {
+ let unrestricted_minting = collection.is_owner_or_admin(sender)?;
+ if !unrestricted_minting {
+ ensure!(
+ collection.mint_mode,
+ <CommonError<T>>::PublicMintingNotAllowed
+ );
+ collection.check_whitelist(sender)?;
+
+ for item in data.iter() {
+ collection.check_whitelist(&item.owner)?;
+ }
+ }
+
+ for data in data.iter() {
+ <PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;
+ if !data.const_data.is_empty() {
+ collection.consume_sstore()?;
+ }
+ if !data.variable_data.is_empty() {
+ collection.consume_sstore()?;
+ }
+ collection.consume_sstore()?;
+ collection.consume_log(3, 0)?;
+ }
+
+ let first_token = <TokensMinted<T>>::get(collection.id);
+ let tokens_minted = first_token
+ .checked_add(data.len() as u32)
+ .ok_or(ArithmeticError::Overflow)?;
+ ensure!(
+ tokens_minted < collection.limits.token_limit,
+ <CommonError<T>>::CollectionTokenLimitExceeded
+ );
+ collection.consume_sstore()?;
+
+ let mut balances = BTreeMap::new();
+ for data in &data {
+ let balance = balances
+ .entry(data.owner.as_sub())
+ .or_insert_with(|| <AccountBalance<T>>::get((collection.id, data.owner.as_sub())));
+ *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;
+
+ ensure!(
+ *balance <= collection.limits.account_token_ownership_limit(),
+ <CommonError<T>>::AccountTokenLimitExceeded,
+ );
+ }
+ collection.consume_sstores(balances.len())?;
+
+ // =========
+
+ <TokensMinted<T>>::insert(collection.id, tokens_minted);
+ for (account, balance) in balances {
+ <AccountBalance<T>>::insert((collection.id, account), balance);
+ }
+ for (i, data) in data.into_iter().enumerate() {
+ let token = first_token + i as u32;
+
+ if !data.const_data.is_empty() {
+ <TokenData<T>>::insert((collection.id, token, DataKind::Constant), data.const_data);
+ }
+ if !data.variable_data.is_empty() {
+ <TokenData<T>>::insert(
+ (collection.id, token, DataKind::Variable),
+ data.variable_data,
+ );
+ }
+ <Owner<T>>::insert((collection.id, token), &data.owner);
+ <Owned<T>>::insert((collection.id, data.owner.as_sub(), token), true);
+
+ collection.log_infallible(ERC721Events::Transfer {
+ from: H160::default(),
+ to: *data.owner.as_eth(),
+ token_id: token.into(),
+ });
+ }
+ Ok(())
+ }
+
+ pub fn set_allowance_unchecked(
+ collection: &NonfungibleHandle<T>,
+ sender: &T::CrossAccountId,
+ token: TokenId,
+ spender: Option<&T::CrossAccountId>,
+ ) {
+ if let Some(spender) = spender {
+ let old_spender = <Allowance<T>>::get((collection.id, token));
+ <Allowance<T>>::insert((collection.id, token), spender);
+ // In ERC721 there is only one possible approved user of token, so we set
+ // approved user to spender
+ collection.log_infallible(ERC721Events::Approval {
+ owner: *sender.as_eth(),
+ approved: *spender.as_eth(),
+ token_id: token.into(),
+ });
+ // In Unique chain, any token can have any amount of approved users, so we need to
+ // set allowance of old owner to 0, and allowance of new owner to 1
+ if old_spender.as_ref() != Some(spender) {
+ if let Some(old_owner) = old_spender {
+ <PalletCommon<T>>::deposit_event(CommonEvent::Approved(
+ collection.id,
+ token,
+ sender.clone(),
+ old_owner.clone(),
+ 0,
+ ));
+ }
+ <PalletCommon<T>>::deposit_event(CommonEvent::Approved(
+ collection.id,
+ token,
+ sender.clone(),
+ spender.clone(),
+ 1,
+ ));
+ }
+ } else {
+ let old_spender = <Allowance<T>>::take((collection.id, token));
+ // In ERC721 there is only one possible approved user of token, so we set
+ // approved user to zero address
+ collection.log_infallible(ERC721Events::Approval {
+ owner: *sender.as_eth(),
+ approved: H160::default(),
+ token_id: token.into(),
+ });
+ // In Unique chain, any token can have any amount of approved users, so we need to
+ // set allowance of old owner to 0
+ if let Some(old_spender) = old_spender {
+ <PalletCommon<T>>::deposit_event(CommonEvent::Approved(
+ collection.id,
+ token,
+ sender.clone(),
+ old_spender.clone(),
+ 0,
+ ));
+ }
+ }
+ }
+
+ pub fn set_allowance(
+ collection: &NonfungibleHandle<T>,
+ sender: &T::CrossAccountId,
+ token: TokenId,
+ spender: Option<&T::CrossAccountId>,
+ ) -> DispatchResult {
+ if collection.access == AccessMode::WhiteList {
+ collection.check_whitelist(&sender)?;
+ if let Some(spender) = spender {
+ collection.check_whitelist(&spender)?;
+ }
+ }
+
+ if let Some(spender) = spender {
+ <PalletCommon<T>>::ensure_correct_receiver(spender)?;
+ }
+ let token_owner = Self::item_owner(collection, token)?;
+ if &token_owner != sender {
+ ensure!(
+ collection.ignores_owned_amount(sender)?,
+ <CommonError<T>>::CantApproveMoreThanOwned
+ );
+ }
+
+ // =========
+
+ Self::set_allowance_unchecked(collection, sender, token, spender);
+ Ok(())
+ }
+
+ pub fn transfer_from(
+ collection: &NonfungibleHandle<T>,
+ spender: &T::CrossAccountId,
+ from: &T::CrossAccountId,
+ to: &T::CrossAccountId,
+ token: TokenId,
+ ) -> DispatchResult {
+ if spender == from {
+ return Self::transfer(collection, from, to, token);
+ }
+ if collection.access == AccessMode::WhiteList {
+ // `from`, `to` checked in [`transfer`]
+ collection.check_whitelist(spender)?;
+ }
+
+ if <Allowance<T>>::get((collection.id, token)).as_ref() != Some(spender) {
+ ensure!(
+ collection.ignores_allowance(spender)?,
+ <CommonError<T>>::TokenValueNotEnough
+ );
+ }
+
+ // =========
+
+ Self::transfer(collection, &from, to, token)?;
+ // Allowance is reset in [`transfer`]
+ Ok(())
+ }
+
+ pub fn set_variable_metadata(
+ collection: &NonfungibleHandle<T>,
+ sender: &T::CrossAccountId,
+ token: TokenId,
+ data: Vec<u8>,
+ ) -> DispatchResult {
+ ensure!(
+ data.len() as u32 <= CUSTOM_DATA_LIMIT,
+ <CommonError<T>>::TokenVariableDataLimitExceeded
+ );
+ let item_owner = Self::item_owner(collection, token)?;
+ collection.check_can_update_meta(sender, &item_owner)?;
+
+ collection.consume_sstore()?;
+
+ // =========
+
+ <TokenData<T>>::insert((collection.id, token, DataKind::Variable), data);
+ Ok(())
+ }
+
+ /// Delegated to `create_multiple_items`
+ pub fn create_item(
+ collection: &NonfungibleHandle<T>,
+ sender: &T::CrossAccountId,
+ data: CreateItemData<T>,
+ ) -> DispatchResult {
+ Self::create_multiple_items(collection, sender, vec![data])
+ }
+}
pallets/nonfungible/src/stubs/UniqueNFT.rawdiffbeforeafterbothbinary blob — no preview
pallets/nonfungible/src/stubs/UniqueNFT.soldiffbeforeafterbothno changes
pallets/nonfungible/src/weights.rsdiffbeforeafterboth--- /dev/null
+++ b/pallets/nonfungible/src/weights.rs
@@ -0,0 +1,37 @@
+#![cfg_attr(rustfmt, rustfmt_skip)]
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+
+use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
+use sp_std::marker::PhantomData;
+
+pub trait WeightInfo {
+ fn create_item() -> Weight;
+ fn create_multiple_items(b: u32) -> Weight;
+ fn burn_item() -> Weight;
+ fn transfer() -> Weight;
+ fn approve() -> Weight;
+ fn transfer_from() -> Weight;
+ fn set_variable_metadata() -> Weight;
+}
+
+pub struct SubstrateWeight<T>(PhantomData<T>);
+impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
+ fn create_item() -> Weight {0}
+ fn create_multiple_items(_b: u32) -> Weight {0}
+ fn burn_item() -> Weight {0}
+ fn transfer() -> Weight {0}
+ fn approve() -> Weight {0}
+ fn transfer_from() -> Weight {0}
+ fn set_variable_metadata() -> Weight {0}
+}
+
+impl WeightInfo for () {
+ fn create_item() -> Weight {0}
+ fn create_multiple_items(_b: u32) -> Weight {0}
+ fn burn_item() -> Weight {0}
+ fn transfer() -> Weight {0}
+ fn approve() -> Weight {0}
+ fn transfer_from() -> Weight {0}
+ fn set_variable_metadata() -> Weight {0}
+}