difftreelog
Merge pull request #317 from UniqueNetwork/feature/CORE-238
in: master
CORE-283 add effective_collection_limits
9 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -9760,6 +9760,55 @@
]
[[package]]
+name = "sc-consensus-epochs"
+version = "0.10.0-dev"
+source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.17#22d40c761a985482f93bbbea5ba4199bdba74f8e"
+dependencies = [
+ "fork-tree",
+ "parity-scale-codec",
+ "sc-client-api",
+ "sc-consensus",
+ "sp-blockchain",
+ "sp-runtime",
+]
+
+[[package]]
+name = "sc-consensus-manual-seal"
+version = "0.10.0-dev"
+source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.17#22d40c761a985482f93bbbea5ba4199bdba74f8e"
+dependencies = [
+ "assert_matches",
+ "async-trait",
+ "futures 0.3.21",
+ "jsonrpc-core",
+ "jsonrpc-core-client",
+ "jsonrpc-derive",
+ "log",
+ "parity-scale-codec",
+ "sc-client-api",
+ "sc-consensus",
+ "sc-consensus-aura",
+ "sc-consensus-babe",
+ "sc-consensus-epochs",
+ "sc-transaction-pool",
+ "sc-transaction-pool-api",
+ "serde",
+ "sp-api",
+ "sp-blockchain",
+ "sp-consensus",
+ "sp-consensus-aura",
+ "sp-consensus-babe",
+ "sp-consensus-slots",
+ "sp-core",
+ "sp-inherents",
+ "sp-keystore",
+ "sp-runtime",
+ "sp-timestamp",
+ "substrate-prometheus-endpoint",
+ "thiserror",
+]
+
+[[package]]
name = "sc-consensus-slots"
version = "0.10.0-dev"
source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.18#fc3fd073d3a0acf9933c3994b660ebd7b5833f65"
@@ -12212,7 +12261,7 @@
"chrono",
"lazy_static",
"matchers",
- "parking_lot 0.11.2",
+ "parking_lot 0.10.2",
"regex",
"serde",
"serde_json",
client/rpc/src/lib.rsdiffbeforeafterboth--- a/client/rpc/src/lib.rs
+++ b/client/rpc/src/lib.rs
@@ -19,7 +19,7 @@
use codec::Decode;
use jsonrpc_core::{Error as RpcError, ErrorCode, Result};
use jsonrpc_derive::rpc;
-use up_data_structs::{Collection, CollectionId, CollectionStats, TokenId};
+use up_data_structs::{Collection, CollectionId, CollectionStats, CollectionLimits, TokenId};
use sp_api::{BlockId, BlockT, ProvideRuntimeApi, ApiExt};
use sp_blockchain::HeaderBackend;
use up_rpc::UniqueApi as UniqueRuntimeApi;
@@ -119,6 +119,12 @@
) -> Result<Option<Collection<AccountId>>>;
#[rpc(name = "unique_collectionStats")]
fn collection_stats(&self, at: Option<BlockHash>) -> Result<CollectionStats>;
+ #[rpc(name = "unique_effectiveCollectionLimits")]
+ fn effective_collection_limits(
+ &self,
+ collection_id: CollectionId,
+ at: Option<BlockHash>,
+ ) -> Result<Option<CollectionLimits>>;
}
pub struct Unique<C, P> {
@@ -222,4 +228,5 @@
pass_method!(last_token_id(collection: CollectionId) -> TokenId);
pass_method!(collection_by_id(collection: CollectionId) -> Option<Collection<AccountId>>);
pass_method!(collection_stats() -> CollectionStats);
+ pass_method!(effective_collection_limits(collection_id: CollectionId) -> Option<CollectionLimits>);
}
pallets/common/src/lib.rsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617#![cfg_attr(not(feature = "std"), no_std)]1819use core::ops::{Deref, DerefMut};20use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};21use sp_std::vec::Vec;22use pallet_evm::account::CrossAccountId;23use frame_support::{24 dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo},25 ensure, fail,26 traits::{Imbalance, Get, Currency},27 BoundedVec,28};29use pallet_evm::GasWeightMapping;30use up_data_structs::{31 COLLECTION_NUMBER_LIMIT, Collection, CollectionId, CreateItemData, ExistenceRequirement,32 MAX_TOKEN_PREFIX_LENGTH, COLLECTION_ADMINS_LIMIT, MetaUpdatePermission, Pays, PostDispatchInfo,33 TokenId, Weight, WithdrawReasons, CollectionStats, MAX_TOKEN_OWNERSHIP, CollectionMode,34 NFT_SPONSOR_TRANSFER_TIMEOUT, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,35 REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, MAX_SPONSOR_TIMEOUT, CUSTOM_DATA_LIMIT, CollectionLimits,36 CustomDataLimit, CreateCollectionData, SponsorshipState, CreateItemExData,37};38pub use pallet::*;39use sp_core::H160;40use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};41#[cfg(feature = "runtime-benchmarks")]42pub mod benchmarking;43pub mod erc;44pub mod eth;4546#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]47pub struct CollectionHandle<T: Config> {48 pub id: CollectionId,49 collection: Collection<T::AccountId>,50 pub recorder: SubstrateRecorder<T>,51}52impl<T: Config> WithRecorder<T> for CollectionHandle<T> {53 fn recorder(&self) -> &SubstrateRecorder<T> {54 &self.recorder55 }56 fn into_recorder(self) -> SubstrateRecorder<T> {57 self.recorder58 }59}60impl<T: Config> CollectionHandle<T> {61 pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {62 <CollectionById<T>>::get(id).map(|collection| Self {63 id,64 collection,65 recorder: SubstrateRecorder::new(eth::collection_id_to_address(id), gas_limit),66 })67 }68 pub fn new(id: CollectionId) -> Option<Self> {69 Self::new_with_gas_limit(id, u64::MAX)70 }71 pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {72 Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)73 }74 pub fn log_mirrored(&self, log: impl evm_coder::ToLog) {75 self.recorder.log_mirrored(log)76 }77 pub fn log_direct(&self, log: impl evm_coder::ToLog) {78 self.recorder.log_direct(log)79 }80 pub fn consume_store_reads(&self, reads: u64) -> evm_coder::execution::Result<()> {81 self.recorder82 .consume_gas(T::GasWeightMapping::weight_to_gas(83 <T as frame_system::Config>::DbWeight::get()84 .read85 .saturating_mul(reads),86 ))87 }88 pub fn consume_store_writes(&self, writes: u64) -> evm_coder::execution::Result<()> {89 self.recorder90 .consume_gas(T::GasWeightMapping::weight_to_gas(91 <T as frame_system::Config>::DbWeight::get()92 .write93 .saturating_mul(writes),94 ))95 }96 pub fn submit_logs(self) {97 self.recorder.submit_logs()98 }99 pub fn save(self) -> DispatchResult {100 self.recorder.submit_logs();101 <CollectionById<T>>::insert(self.id, self.collection);102 Ok(())103 }104}105impl<T: Config> Deref for CollectionHandle<T> {106 type Target = Collection<T::AccountId>;107108 fn deref(&self) -> &Self::Target {109 &self.collection110 }111}112113impl<T: Config> DerefMut for CollectionHandle<T> {114 fn deref_mut(&mut self) -> &mut Self::Target {115 &mut self.collection116 }117}118119impl<T: Config> CollectionHandle<T> {120 pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult {121 ensure!(*subject.as_sub() == self.owner, <Error<T>>::NoPermission);122 Ok(())123 }124 pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> bool {125 *subject.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, subject))126 }127 pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult {128 ensure!(self.is_owner_or_admin(subject), <Error<T>>::NoPermission);129 Ok(())130 }131 pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool {132 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)133 }134 pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {135 self.limits.owner_can_transfer() && self.is_owner_or_admin(user)136 }137 pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {138 ensure!(139 <Allowlist<T>>::get((self.id, user)),140 <Error<T>>::AddressNotInAllowlist141 );142 Ok(())143 }144145 pub fn check_can_update_meta(146 &self,147 subject: &T::CrossAccountId,148 item_owner: &T::CrossAccountId,149 ) -> DispatchResult {150 match self.meta_update_permission {151 MetaUpdatePermission::ItemOwner => {152 ensure!(subject == item_owner, <Error<T>>::NoPermission);153 Ok(())154 }155 MetaUpdatePermission::Admin => self.check_is_owner_or_admin(subject),156 MetaUpdatePermission::None => fail!(<Error<T>>::NoPermission),157 }158 }159}160161#[frame_support::pallet]162pub mod pallet {163 use super::*;164 use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key};165 use pallet_evm::account;166 use frame_support::traits::Currency;167 use up_data_structs::TokenId;168 use scale_info::TypeInfo;169170 #[pallet::config]171 pub trait Config:172 frame_system::Config + pallet_evm_coder_substrate::Config + TypeInfo + account::Config173 {174 type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;175176 type Currency: Currency<Self::AccountId>;177178 #[pallet::constant]179 type CollectionCreationPrice: Get<180 <<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,181 >;182183 type TreasuryAccountId: Get<Self::AccountId>;184 }185186 #[pallet::pallet]187 #[pallet::generate_store(pub(super) trait Store)]188 pub struct Pallet<T>(_);189190 #[pallet::extra_constants]191 impl<T: Config> Pallet<T> {192 pub fn collection_admins_limit() -> u32 {193 COLLECTION_ADMINS_LIMIT194 }195 }196197 #[pallet::event]198 #[pallet::generate_deposit(pub fn deposit_event)]199 pub enum Event<T: Config> {200 /// New collection was created201 ///202 /// # Arguments203 ///204 /// * collection_id: Globally unique identifier of newly created collection.205 ///206 /// * mode: [CollectionMode] converted into u8.207 ///208 /// * account_id: Collection owner.209 CollectionCreated(CollectionId, u8, T::AccountId),210211 /// New collection was destroyed212 ///213 /// # Arguments214 ///215 /// * collection_id: Globally unique identifier of collection.216 CollectionDestroyed(CollectionId),217218 /// New item was created.219 ///220 /// # Arguments221 ///222 /// * collection_id: Id of the collection where item was created.223 ///224 /// * item_id: Id of an item. Unique within the collection.225 ///226 /// * recipient: Owner of newly created item227 ///228 /// * amount: Always 1 for NFT229 ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),230231 /// Collection item was burned.232 ///233 /// # Arguments234 ///235 /// * collection_id.236 ///237 /// * item_id: Identifier of burned NFT.238 ///239 /// * owner: which user has destroyed its tokens240 ///241 /// * amount: Always 1 for NFT242 ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),243244 /// Item was transferred245 ///246 /// * collection_id: Id of collection to which item is belong247 ///248 /// * item_id: Id of an item249 ///250 /// * sender: Original owner of item251 ///252 /// * recipient: New owner of item253 ///254 /// * amount: Always 1 for NFT255 Transfer(256 CollectionId,257 TokenId,258 T::CrossAccountId,259 T::CrossAccountId,260 u128,261 ),262263 /// * collection_id264 ///265 /// * item_id266 ///267 /// * sender268 ///269 /// * spender270 ///271 /// * amount272 Approved(273 CollectionId,274 TokenId,275 T::CrossAccountId,276 T::CrossAccountId,277 u128,278 ),279 }280281 #[pallet::error]282 pub enum Error<T> {283 /// This collection does not exist.284 CollectionNotFound,285 /// Sender parameter and item owner must be equal.286 MustBeTokenOwner,287 /// No permission to perform action288 NoPermission,289 /// Collection is not in mint mode.290 PublicMintingNotAllowed,291 /// Address is not in allow list.292 AddressNotInAllowlist,293294 /// Collection name can not be longer than 63 char.295 CollectionNameLimitExceeded,296 /// Collection description can not be longer than 255 char.297 CollectionDescriptionLimitExceeded,298 /// Token prefix can not be longer than 15 char.299 CollectionTokenPrefixLimitExceeded,300 /// Total collections bound exceeded.301 TotalCollectionsLimitExceeded,302 /// variable_data exceeded data limit.303 TokenVariableDataLimitExceeded,304 /// Exceeded max admin count305 CollectionAdminCountExceeded,306 /// Collection limit bounds per collection exceeded307 CollectionLimitBoundsExceeded,308 /// Tried to enable permissions which are only permitted to be disabled309 OwnerPermissionsCantBeReverted,310311 /// Collection settings not allowing items transferring312 TransferNotAllowed,313 /// Account token limit exceeded per collection314 AccountTokenLimitExceeded,315 /// Collection token limit exceeded316 CollectionTokenLimitExceeded,317 /// Metadata flag frozen318 MetadataFlagFrozen,319320 /// Item not exists.321 TokenNotFound,322 /// Item balance not enough.323 TokenValueTooLow,324 /// Requested value more than approved.325 ApprovedValueTooLow,326 /// Tried to approve more than owned327 CantApproveMoreThanOwned,328329 /// Can't transfer tokens to ethereum zero address330 AddressIsZero,331 /// Target collection doesn't supports this operation332 UnsupportedOperation,333334 /// Not sufficient founds to perform action335 NotSufficientFounds,336 }337338 #[pallet::storage]339 pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;340 #[pallet::storage]341 pub type DestroyedCollectionCount<T> =342 StorageValue<Value = CollectionId, QueryKind = ValueQuery>;343344 /// Collection info345 #[pallet::storage]346 pub type CollectionById<T> = StorageMap<347 Hasher = Blake2_128Concat,348 Key = CollectionId,349 Value = Collection<<T as frame_system::Config>::AccountId>,350 QueryKind = OptionQuery,351 >;352353 #[pallet::storage]354 pub type AdminAmount<T> = StorageMap<355 Hasher = Blake2_128Concat,356 Key = CollectionId,357 Value = u32,358 QueryKind = ValueQuery,359 >;360361 /// List of collection admins362 #[pallet::storage]363 pub type IsAdmin<T: Config> = StorageNMap<364 Key = (365 Key<Blake2_128Concat, CollectionId>,366 Key<Blake2_128Concat, T::CrossAccountId>,367 ),368 Value = bool,369 QueryKind = ValueQuery,370 >;371372 /// Allowlisted collection users373 #[pallet::storage]374 pub type Allowlist<T: Config> = StorageNMap<375 Key = (376 Key<Blake2_128Concat, CollectionId>,377 Key<Blake2_128Concat, T::CrossAccountId>,378 ),379 Value = bool,380 QueryKind = ValueQuery,381 >;382383 /// Not used by code, exists only to provide some types to metadata384 #[pallet::storage]385 pub type DummyStorageValue<T> =386 StorageValue<Value = (CollectionStats, CollectionId, TokenId), QueryKind = OptionQuery>;387}388389impl<T: Config> Pallet<T> {390 /// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens391 pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {392 ensure!(393 &T::CrossAccountId::from_eth(H160([0; 20])) != receiver,394 <Error<T>>::AddressIsZero395 );396 Ok(())397 }398 pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {399 <IsAdmin<T>>::iter_prefix((collection,))400 .map(|(a, _)| a)401 .collect()402 }403 pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {404 <Allowlist<T>>::iter_prefix((collection,))405 .map(|(a, _)| a)406 .collect()407 }408 pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {409 <Allowlist<T>>::get((collection, user))410 }411 pub fn collection_stats() -> CollectionStats {412 let created = <CreatedCollectionCount<T>>::get();413 let destroyed = <DestroyedCollectionCount<T>>::get();414 CollectionStats {415 created: created.0,416 destroyed: destroyed.0,417 alive: created.0 - destroyed.0,418 }419 }420}421422impl<T: Config> Pallet<T> {423 pub fn init_collection(424 owner: T::AccountId,425 data: CreateCollectionData<T::AccountId>,426 ) -> Result<CollectionId, DispatchError> {427 {428 ensure!(429 data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH as usize,430 Error::<T>::CollectionTokenPrefixLimitExceeded431 );432 }433434 let created_count = <CreatedCollectionCount<T>>::get()435 .0436 .checked_add(1)437 .ok_or(ArithmeticError::Overflow)?;438 let destroyed_count = <DestroyedCollectionCount<T>>::get().0;439 let id = CollectionId(created_count);440441 // bound Total number of collections442 ensure!(443 created_count - destroyed_count <= COLLECTION_NUMBER_LIMIT,444 <Error<T>>::TotalCollectionsLimitExceeded445 );446447 // =========448449 let collection = Collection {450 owner: owner.clone(),451 name: data.name,452 mode: data.mode.clone(),453 mint_mode: false,454 access: data.access.unwrap_or_default(),455 description: data.description,456 token_prefix: data.token_prefix,457 offchain_schema: data.offchain_schema,458 schema_version: data.schema_version.unwrap_or_default(),459 sponsorship: data460 .pending_sponsor461 .map(SponsorshipState::Unconfirmed)462 .unwrap_or_default(),463 variable_on_chain_schema: data.variable_on_chain_schema,464 const_on_chain_schema: data.const_on_chain_schema,465 limits: data466 .limits467 .map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits))468 .unwrap_or_else(|| Ok(CollectionLimits::default()))?,469 meta_update_permission: data.meta_update_permission.unwrap_or_default(),470 };471472 // Take a (non-refundable) deposit of collection creation473 {474 let mut imbalance =475 <<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();476 imbalance.subsume(477 <<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(478 &T::TreasuryAccountId::get(),479 T::CollectionCreationPrice::get(),480 ),481 );482 <T as Config>::Currency::settle(483 &owner,484 imbalance,485 WithdrawReasons::TRANSFER,486 ExistenceRequirement::KeepAlive,487 )488 .map_err(|_| Error::<T>::NotSufficientFounds)?;489 }490491 <CreatedCollectionCount<T>>::put(created_count);492 <Pallet<T>>::deposit_event(Event::CollectionCreated(id, data.mode.id(), owner.clone()));493 <CollectionById<T>>::insert(id, collection);494 Ok(id)495 }496497 pub fn destroy_collection(498 collection: CollectionHandle<T>,499 sender: &T::CrossAccountId,500 ) -> DispatchResult {501 ensure!(502 collection.limits.owner_can_destroy(),503 <Error<T>>::NoPermission,504 );505 collection.check_is_owner(sender)?;506507 let destroyed_collections = <DestroyedCollectionCount<T>>::get()508 .0509 .checked_add(1)510 .ok_or(ArithmeticError::Overflow)?;511512 // =========513514 <DestroyedCollectionCount<T>>::put(destroyed_collections);515 <CollectionById<T>>::remove(collection.id);516 <AdminAmount<T>>::remove(collection.id);517 <IsAdmin<T>>::remove_prefix((collection.id,), None);518 <Allowlist<T>>::remove_prefix((collection.id,), None);519520 <Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));521 Ok(())522 }523524 pub fn toggle_allowlist(525 collection: &CollectionHandle<T>,526 sender: &T::CrossAccountId,527 user: &T::CrossAccountId,528 allowed: bool,529 ) -> DispatchResult {530 collection.check_is_owner_or_admin(sender)?;531532 // =========533534 if allowed {535 <Allowlist<T>>::insert((collection.id, user), true);536 } else {537 <Allowlist<T>>::remove((collection.id, user));538 }539540 Ok(())541 }542543 pub fn toggle_admin(544 collection: &CollectionHandle<T>,545 sender: &T::CrossAccountId,546 user: &T::CrossAccountId,547 admin: bool,548 ) -> DispatchResult {549 collection.check_is_owner_or_admin(sender)?;550551 let was_admin = <IsAdmin<T>>::get((collection.id, user));552 if was_admin == admin {553 return Ok(());554 }555 let amount = <AdminAmount<T>>::get(collection.id);556557 if admin {558 let amount = amount559 .checked_add(1)560 .ok_or(<Error<T>>::CollectionAdminCountExceeded)?;561 ensure!(562 amount <= Self::collection_admins_limit(),563 <Error<T>>::CollectionAdminCountExceeded,564 );565566 // =========567568 <AdminAmount<T>>::insert(collection.id, amount);569 <IsAdmin<T>>::insert((collection.id, user), true);570 } else {571 <AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));572 <IsAdmin<T>>::remove((collection.id, user));573 }574575 Ok(())576 }577578 pub fn clamp_limits(579 mode: CollectionMode,580 old_limit: &CollectionLimits,581 mut new_limit: CollectionLimits,582 ) -> Result<CollectionLimits, DispatchError> {583 macro_rules! limit_default {584 ($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{585 $(586 if let Some($new) = $new.$field {587 let $old = $old.$field($($arg)?);588 let _ = $new;589 let _ = $old;590 $check591 } else {592 $new.$field = $old.$field593 }594 )*595 }};596 }597598 limit_default!(old_limit, new_limit,599 account_token_ownership_limit => ensure!(600 new_limit <= MAX_TOKEN_OWNERSHIP,601 <Error<T>>::CollectionLimitBoundsExceeded,602 ),603 sponsor_transfer_timeout(match mode {604 CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,605 CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,606 CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,607 }) => ensure!(608 new_limit <= MAX_SPONSOR_TIMEOUT,609 <Error<T>>::CollectionLimitBoundsExceeded,610 ),611 sponsored_data_size => ensure!(612 new_limit <= CUSTOM_DATA_LIMIT,613 <Error<T>>::CollectionLimitBoundsExceeded,614 ),615 token_limit => ensure!(616 old_limit >= new_limit && new_limit > 0,617 <Error<T>>::CollectionTokenLimitExceeded618 ),619 owner_can_transfer => ensure!(620 old_limit || !new_limit,621 <Error<T>>::OwnerPermissionsCantBeReverted,622 ),623 owner_can_destroy => ensure!(624 old_limit || !new_limit,625 <Error<T>>::OwnerPermissionsCantBeReverted,626 ),627 sponsored_data_rate_limit => {},628 transfers_enabled => {},629 );630 Ok(new_limit)631 }632}633634#[macro_export]635macro_rules! unsupported {636 () => {637 Err(<Error<T>>::UnsupportedOperation.into())638 };639}640641/// Worst cases642pub trait CommonWeightInfo<CrossAccountId> {643 fn create_item() -> Weight;644 fn create_multiple_items(amount: u32) -> Weight;645 fn create_multiple_items_ex(cost: &CreateItemExData<CrossAccountId>) -> Weight;646 fn burn_item() -> Weight;647 fn transfer() -> Weight;648 fn approve() -> Weight;649 fn transfer_from() -> Weight;650 fn burn_from() -> Weight;651 fn set_variable_metadata(bytes: u32) -> Weight;652}653654pub trait CommonCollectionOperations<T: Config> {655 fn create_item(656 &self,657 sender: T::CrossAccountId,658 to: T::CrossAccountId,659 data: CreateItemData,660 ) -> DispatchResultWithPostInfo;661 fn create_multiple_items(662 &self,663 sender: T::CrossAccountId,664 to: T::CrossAccountId,665 data: Vec<CreateItemData>,666 ) -> DispatchResultWithPostInfo;667 fn create_multiple_items_ex(668 &self,669 sender: T::CrossAccountId,670 data: CreateItemExData<T::CrossAccountId>,671 ) -> DispatchResultWithPostInfo;672 fn burn_item(673 &self,674 sender: T::CrossAccountId,675 token: TokenId,676 amount: u128,677 ) -> DispatchResultWithPostInfo;678679 fn transfer(680 &self,681 sender: T::CrossAccountId,682 to: T::CrossAccountId,683 token: TokenId,684 amount: u128,685 ) -> DispatchResultWithPostInfo;686 fn approve(687 &self,688 sender: T::CrossAccountId,689 spender: T::CrossAccountId,690 token: TokenId,691 amount: u128,692 ) -> DispatchResultWithPostInfo;693 fn transfer_from(694 &self,695 sender: T::CrossAccountId,696 from: T::CrossAccountId,697 to: T::CrossAccountId,698 token: TokenId,699 amount: u128,700 ) -> DispatchResultWithPostInfo;701 fn burn_from(702 &self,703 sender: T::CrossAccountId,704 from: T::CrossAccountId,705 token: TokenId,706 amount: u128,707 ) -> DispatchResultWithPostInfo;708709 fn set_variable_metadata(710 &self,711 sender: T::CrossAccountId,712 token: TokenId,713 data: BoundedVec<u8, CustomDataLimit>,714 ) -> DispatchResultWithPostInfo;715716 fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;717 fn token_exists(&self, token: TokenId) -> bool;718 fn last_token_id(&self) -> TokenId;719720 fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId>;721 fn const_metadata(&self, token: TokenId) -> Vec<u8>;722 fn variable_metadata(&self, token: TokenId) -> Vec<u8>;723724 /// How many tokens collection contains (Applicable to nonfungible/refungible)725 fn collection_tokens(&self) -> u32;726 /// Amount of different tokens account has (Applicable to nonfungible/refungible)727 fn account_balance(&self, account: T::CrossAccountId) -> u32;728 /// Amount of specific token account have (Applicable to fungible/refungible)729 fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;730 fn allowance(731 &self,732 sender: T::CrossAccountId,733 spender: T::CrossAccountId,734 token: TokenId,735 ) -> u128;736}737738// Flexible enough for implementing CommonCollectionOperations739pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {740 let post_info = PostDispatchInfo {741 actual_weight: Some(weight),742 pays_fee: Pays::Yes,743 };744 match res {745 Ok(()) => Ok(post_info),746 Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),747 }748}primitives/rpc/src/lib.rsdiffbeforeafterboth--- a/primitives/rpc/src/lib.rs
+++ b/primitives/rpc/src/lib.rs
@@ -16,7 +16,7 @@
#![cfg_attr(not(feature = "std"), no_std)]
-use up_data_structs::{CollectionId, TokenId, Collection, CollectionStats};
+use up_data_structs::{CollectionId, TokenId, Collection, CollectionStats, CollectionLimits};
use sp_std::vec::Vec;
use sp_core::H160;
use codec::Decode;
@@ -59,5 +59,6 @@
fn last_token_id(collection: CollectionId) -> Result<TokenId>;
fn collection_by_id(collection: CollectionId) -> Result<Option<Collection<AccountId>>>;
fn collection_stats() -> Result<CollectionStats>;
+ fn effective_collection_limits(collection_id: CollectionId) -> Result<Option<CollectionLimits>>;
}
}
runtime/common/src/runtime_apis.rsdiffbeforeafterboth--- a/runtime/common/src/runtime_apis.rs
+++ b/runtime/common/src/runtime_apis.rs
@@ -69,6 +69,10 @@
fn collection_stats() -> Result<CollectionStats, DispatchError> {
Ok(<pallet_common::Pallet<Runtime>>::collection_stats())
}
+
+ fn effective_collection_limits(collection: CollectionId) -> Result<Option<CollectionLimits>, DispatchError> {
+ Ok(<pallet_common::Pallet<Runtime>>::effective_collection_limits(collection))
+ }
}
impl sp_api::Core<Block> for Runtime {
tests/package.jsondiffbeforeafterboth--- a/tests/package.json
+++ b/tests/package.json
@@ -67,6 +67,7 @@
"testPalletPresence": "mocha --timeout 9999999 -r ts-node/register ./**/pallet-presence.test.ts",
"testBlockProduction": "mocha --timeout 9999999 -r ts-node/register ./**/block-production.test.ts",
"testEnableDisableTransfers": "mocha --timeout 9999999 -r ts-node/register ./**/enableDisableTransfer.test.ts",
+ "testLimits": "mocha --timeout 9999999 -r ts-node/register ./**/limits.test.ts",
"polkadot-types-fetch-metadata": "curl -H 'Content-Type: application/json' -d '{\"id\":\"1\", \"jsonrpc\":\"2.0\", \"method\": \"state_getMetadata\", \"params\":[]}' http://localhost:9933 > src/interfaces/metadata.json",
"polkadot-types-from-defs": "echo 'export default {}' > src/interfaces/lookup.ts && ts-node ./node_modules/.bin/polkadot-types-from-defs --endpoint src/interfaces/metadata.json --input src/interfaces/ --package .",
"polkadot-types-from-chain": "ts-node ./node_modules/.bin/polkadot-types-from-chain --endpoint src/interfaces/metadata.json --output src/interfaces/ --package .",
tests/src/interfaces/augment-api-rpc.tsdiffbeforeafterboth--- a/tests/src/interfaces/augment-api-rpc.ts
+++ b/tests/src/interfaces/augment-api-rpc.ts
@@ -1,7 +1,7 @@
// Auto-generated via `yarn polkadot-types-from-chain`, do not edit
/* eslint-disable */
-import type { PalletEvmAccountBasicCrossAccountIdRepr, UpDataStructsCollection, UpDataStructsCollectionStats } from './unique';
+import type { PalletEvmAccountBasicCrossAccountIdRepr, UpDataStructsCollection, UpDataStructsCollectionStats, UpDataStructsCollectionStats } from './unique';
import type { AugmentedRpc } from '@polkadot/rpc-core/types';
import type { Metadata, StorageKey } from '@polkadot/types';
import type { Bytes, HashMap, Json, Null, Option, Text, U256, U64, Vec, bool, u128, u32, u64 } from '@polkadot/types-codec';
@@ -604,6 +604,10 @@
**/
constMetadata: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<Bytes>>;
/**
+ * Get effective collection limits
+ **/
+ effectiveCollectionLimits: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<Option<UpDataStructsCollectionLimits>>>;
+ /**
* Get last token id
**/
lastTokenId: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<u32>>;
tests/src/interfaces/unique/definitions.tsdiffbeforeafterboth--- a/tests/src/interfaces/unique/definitions.ts
+++ b/tests/src/interfaces/unique/definitions.ts
@@ -55,5 +55,6 @@
collectionById: fun('Get collection by specified id', [collectionParam], 'Option<UpDataStructsCollection>'),
collectionStats: fun('Get collection stats', [], 'UpDataStructsCollectionStats'),
allowed: fun('Check if user is allowed to use collection', [collectionParam, crossAccountParam()], 'bool'),
+ effectiveCollectionLimits: fun('Get effective collection limits', [collectionParam], 'Option<UpDataStructsCollectionLimits>'),
},
};
tests/src/limits.test.tsdiffbeforeafterboth--- a/tests/src/limits.test.ts
+++ b/tests/src/limits.test.ts
@@ -395,4 +395,73 @@
expect(aliceBalanceAfterSponsoredTransaction1 < aliceBalanceBefore).to.be.true;
//expect(aliceBalanceAfterSponsoredTransaction1).to.be.lessThan(aliceBalanceBefore);
});
+
+ it('Effective collection limits', async () => {
+ await usingApi(async (api) => {
+ const collectionId = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
+
+ { // Check that limits is undefined
+ const collection = await api.rpc.unique.collectionById(collectionId);
+ expect(collection.isSome).to.be.true;
+ const limits = collection.unwrap().limits;
+ expect(limits).to.be.any;
+
+ expect(limits.accountTokenOwnershipLimit.toHuman()).to.be.null;
+ expect(limits.sponsoredDataSize.toHuman()).to.be.null;
+ expect(limits.sponsoredDataRateLimit.toHuman()).to.be.null;
+ expect(limits.tokenLimit.toHuman()).to.be.null;
+ expect(limits.sponsorTransferTimeout.toHuman()).to.be.null;
+ expect(limits.sponsorApproveTimeout.toHuman()).to.be.null;
+ expect(limits.ownerCanTransfer.toHuman()).to.be.null;
+ expect(limits.ownerCanDestroy.toHuman()).to.be.null;
+ expect(limits.transfersEnabled.toHuman()).to.be.null;
+ }
+
+ { // Check that limits is undefined for non-existent collection
+ const limits = await api.rpc.unique.effectiveCollectionLimits(11111);
+ expect(limits.toHuman()).to.be.null;
+ }
+
+ { // Check that default values defined for collection limits
+ const limitsOpt = await api.rpc.unique.effectiveCollectionLimits(collectionId);
+ expect(limitsOpt.isNone).to.be.false;
+ const limits = limitsOpt.unwrap();
+
+ expect(limits.accountTokenOwnershipLimit.toHuman()).to.be.eq('100,000');
+ expect(limits.sponsoredDataSize.toHuman()).to.be.eq('2,048');
+ expect(limits.sponsoredDataRateLimit.toHuman()).to.be.eq('SponsoringDisabled');
+ expect(limits.tokenLimit.toHuman()).to.be.eq('4,294,967,295');
+ expect(limits.sponsorTransferTimeout.toHuman()).to.be.eq('5');
+ expect(limits.sponsorApproveTimeout.toHuman()).to.be.eq('5');
+ expect(limits.ownerCanTransfer.toHuman()).to.be.true;
+ expect(limits.ownerCanDestroy.toHuman()).to.be.true;
+ expect(limits.transfersEnabled.toHuman()).to.be.true;
+ }
+
+ { //Check the values for collection limits
+ await setCollectionLimitsExpectSuccess(alice, collectionId, {
+ accountTokenOwnershipLimit: 99_999,
+ sponsoredDataSize: 1024,
+ tokenLimit: 123,
+ transfersEnabled: false,
+ });
+
+ const limitsOpt = await api.rpc.unique.effectiveCollectionLimits(collectionId);
+ expect(limitsOpt.isNone).to.be.false;
+ const limits = limitsOpt.unwrap();
+
+ expect(limits.accountTokenOwnershipLimit.toHuman()).to.be.eq('99,999');
+ expect(limits.sponsoredDataSize.toHuman()).to.be.eq('1,024');
+ expect(limits.sponsoredDataRateLimit.toHuman()).to.be.eq('SponsoringDisabled');
+ expect(limits.tokenLimit.toHuman()).to.be.eq('123');
+ expect(limits.sponsorTransferTimeout.toHuman()).to.be.eq('5');
+ expect(limits.sponsorApproveTimeout.toHuman()).to.be.eq('5');
+ expect(limits.ownerCanTransfer.toHuman()).to.be.true;
+ expect(limits.ownerCanDestroy.toHuman()).to.be.true;
+ expect(limits.transfersEnabled.toHuman()).to.be.false;
+ }
+ });
+ });
});
+
+