difftreelog
Merge pull request #337 from UniqueNetwork/CORE-178
in: master
feature/core-178
9 files changed
client/rpc/src/lib.rsdiffbeforeafterboth--- a/client/rpc/src/lib.rs
+++ b/client/rpc/src/lib.rs
@@ -119,6 +119,15 @@
) -> Result<Option<Collection<AccountId>>>;
#[rpc(name = "unique_collectionStats")]
fn collection_stats(&self, at: Option<BlockHash>) -> Result<CollectionStats>;
+
+ #[rpc(name = "unique_nextSponsored")]
+ fn next_sponsored(
+ &self,
+ collection: CollectionId,
+ account: CrossAccountId,
+ token: TokenId,
+ at: Option<BlockHash>,
+ ) -> Result<Option<u64>>;
#[rpc(name = "unique_effectiveCollectionLimits")]
fn effective_collection_limits(
&self,
@@ -228,5 +237,6 @@
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!(next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Option<u64>);
pass_method!(effective_collection_limits(collection_id: CollectionId) -> Option<CollectionLimits>);
}
pallets/unique/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#![recursion_limit = "1024"]18#![cfg_attr(not(feature = "std"), no_std)]19#![allow(20 clippy::too_many_arguments,21 clippy::unnecessary_mut_passed,22 clippy::unused_unit23)]2425extern crate alloc;2627pub use serde::{Serialize, Deserialize};2829pub use frame_support::{30 construct_runtime, decl_module, decl_storage, decl_error, decl_event,31 dispatch::DispatchResult,32 ensure, fail, parameter_types,33 traits::{34 ExistenceRequirement, Get, Imbalance, KeyOwnerProofSystem, OnUnbalanced, Randomness,35 IsSubType, WithdrawReasons,36 },37 weights::{38 constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND},39 DispatchInfo, GetDispatchInfo, IdentityFee, Pays, PostDispatchInfo, Weight,40 WeightToFeePolynomial, DispatchClass,41 },42 StorageValue, transactional,43 pallet_prelude::{DispatchResultWithPostInfo, ConstU32},44 BoundedVec,45};46use scale_info::TypeInfo;47use frame_system::{self as system, ensure_signed};48use sp_runtime::{sp_std::prelude::Vec};49use up_data_structs::{50 MAX_DECIMAL_POINTS, VARIABLE_ON_CHAIN_SCHEMA_LIMIT, CONST_ON_CHAIN_SCHEMA_LIMIT,51 OFFCHAIN_SCHEMA_LIMIT, MAX_COLLECTION_NAME_LENGTH, MAX_COLLECTION_DESCRIPTION_LENGTH,52 MAX_TOKEN_PREFIX_LENGTH, AccessMode, CreateItemData, CollectionLimits, CollectionId,53 CollectionMode, TokenId, SchemaVersion, SponsorshipState, MetaUpdatePermission,54 CreateCollectionData, CustomDataLimit, CreateItemExData,55};56use pallet_common::{CollectionHandle, Pallet as PalletCommon, Error as CommonError, CommonWeightInfo};57use pallet_evm::account::CrossAccountId;58use pallet_refungible::{Pallet as PalletRefungible, RefungibleHandle};59use pallet_fungible::{Pallet as PalletFungible, FungibleHandle};60use pallet_nonfungible::{Pallet as PalletNonfungible, NonfungibleHandle};6162#[cfg(test)]63mod mock;6465#[cfg(test)]66mod tests;6768mod eth;69mod sponsorship;70pub use sponsorship::UniqueSponsorshipHandler;71pub use eth::sponsoring::UniqueEthSponsorshipHandler;7273pub use eth::UniqueErcSupport;7475pub mod common;76use common::CommonWeights;77pub mod dispatch;78use dispatch::dispatch_call;7980#[cfg(feature = "runtime-benchmarks")]81mod benchmarking;82pub mod weights;83use weights::WeightInfo;8485decl_error! {86 /// Error for non-fungible-token module.87 pub enum Error for Module<T: Config> {88 /// Decimal_points parameter must be lower than MAX_DECIMAL_POINTS constant, currently it is 30.89 CollectionDecimalPointLimitExceeded,90 /// This address is not set as sponsor, use setCollectionSponsor first.91 ConfirmUnsetSponsorFail,92 /// Length of items properties must be greater than 0.93 EmptyArgument,94 }95}9697pub trait Config:98 system::Config99 + pallet_evm_coder_substrate::Config100 + pallet_common::Config101 + pallet_nonfungible::Config102 + pallet_refungible::Config103 + pallet_fungible::Config104 + Sized105 + TypeInfo106{107 type Event: From<Event<Self>> + Into<<Self as frame_system::Config>::Event>;108109 /// Weight information for extrinsics in this pallet.110 type WeightInfo: WeightInfo;111}112113decl_event! {114 pub enum Event<T>115 where116 <T as frame_system::Config>::AccountId,117 <T as pallet_evm::account::Config>::CrossAccountId,118 {119 /// Collection sponsor was removed120 ///121 /// # Arguments122 ///123 /// * collection_id: Globally unique collection identifier.124 CollectionSponsorRemoved(CollectionId),125126 /// Collection admin was added127 ///128 /// # Arguments129 ///130 /// * collection_id: Globally unique collection identifier.131 ///132 /// * admin: Admin address.133 CollectionAdminAdded(CollectionId, CrossAccountId),134135 /// Collection owned was change136 ///137 /// # Arguments138 ///139 /// * collection_id: Globally unique collection identifier.140 ///141 /// * owner: New owner address.142 CollectionOwnedChanged(CollectionId, AccountId),143144 /// Collection sponsor was set145 ///146 /// # Arguments147 ///148 /// * collection_id: Globally unique collection identifier.149 ///150 /// * owner: New sponsor address.151 CollectionSponsorSet(CollectionId, AccountId),152153 /// const on chain schema was set154 ///155 /// # Arguments156 ///157 /// * collection_id: Globally unique collection identifier.158 ConstOnChainSchemaSet(CollectionId),159160 /// New sponsor was confirm161 ///162 /// # Arguments163 ///164 /// * collection_id: Globally unique collection identifier.165 ///166 /// * sponsor: New sponsor address.167 SponsorshipConfirmed(CollectionId, AccountId),168169 /// Collection admin was removed170 ///171 /// # Arguments172 ///173 /// * collection_id: Globally unique collection identifier.174 ///175 /// * admin: Admin address.176 CollectionAdminRemoved(CollectionId, CrossAccountId),177178 /// Address was remove from allow list179 ///180 /// # Arguments181 ///182 /// * collection_id: Globally unique collection identifier.183 ///184 /// * user: Address.185 AllowListAddressRemoved(CollectionId, CrossAccountId),186187 /// Address was add to allow list188 ///189 /// # Arguments190 ///191 /// * collection_id: Globally unique collection identifier.192 ///193 /// * user: Address.194 AllowListAddressAdded(CollectionId, CrossAccountId),195196 /// Collection limits was set197 ///198 /// # Arguments199 ///200 /// * collection_id: Globally unique collection identifier.201 CollectionLimitSet(CollectionId),202203 /// Mint permission was set204 ///205 /// # Arguments206 ///207 /// * collection_id: Globally unique collection identifier.208 MintPermissionSet(CollectionId),209210 /// Offchain schema was set211 ///212 /// # Arguments213 ///214 /// * collection_id: Globally unique collection identifier.215 OffchainSchemaSet(CollectionId),216217 /// Public access mode was set218 ///219 /// # Arguments220 ///221 /// * collection_id: Globally unique collection identifier.222 ///223 /// * mode: New access state.224 PublicAccessModeSet(CollectionId, AccessMode),225226 /// Schema version was set227 ///228 /// # Arguments229 ///230 /// * collection_id: Globally unique collection identifier.231 SchemaVersionSet(CollectionId),232233 /// Variable on chain schema was set234 ///235 /// # Arguments236 ///237 /// * collection_id: Globally unique collection identifier.238 VariableOnChainSchemaSet(CollectionId),239 }240}241242type SelfWeightOf<T> = <T as Config>::WeightInfo;243244// # Used definitions245//246// ## User control levels247//248// chain-controlled - key is uncontrolled by user249// i.e autoincrementing index250// can use non-cryptographic hash251// real - key is controlled by user252// but it is hard to generate enough colliding values, i.e owner of signed txs253// can use non-cryptographic hash254// controlled - key is completly controlled by users255// i.e maps with mutable keys256// should use cryptographic hash257//258// ## User control level downgrade reasons259//260// ?1 - chain-controlled -> controlled261// collections/tokens can be destroyed, resulting in massive holes262// ?2 - chain-controlled -> controlled263// same as ?1, but can be only added, resulting in easier exploitation264// ?3 - real -> controlled265// no confirmation required, so addresses can be easily generated266decl_storage! {267 trait Store for Module<T: Config> as Unique {268269 //#region Private members270 /// Used for migrations271 ChainVersion: u64;272 //#endregion273274 //#region Tokens transfer rate limit baskets275 /// (Collection id (controlled?2), who created (real))276 /// TODO: Off chain worker should remove from this map when collection gets removed277 pub CreateItemBasket get(fn create_item_basket): map hasher(blake2_128_concat) (CollectionId, T::AccountId) => Option<T::BlockNumber>;278 /// Collection id (controlled?2), token id (controlled?2)279 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;280 /// Collection id (controlled?2), owning user (real)281 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;282 /// Collection id (controlled?2), token id (controlled?2)283 pub ReFungibleTransferBasket get(fn refungible_transfer_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;284 //#endregion285286 /// Variable metadata sponsoring287 /// Collection id (controlled?2), token id (controlled?2)288 pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;289 /// Approval sponsoring290 pub NftApproveBasket get(fn nft_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;291 pub FungibleApproveBasket get(fn fungible_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;292 pub RefungibleApproveBasket get(fn refungible_approve_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;293 }294}295296decl_module! {297 pub struct Module<T: Config> for enum Call298 where299 origin: T::Origin300 {301 type Error = Error<T>;302303 fn deposit_event() = default;304305 fn on_initialize(_now: T::BlockNumber) -> Weight {306 0307 }308309 /// This method creates a Collection of NFTs. Each Token may have multiple properties encoded as an array of bytes of certain length. The initial owner of the collection is set to the address that signed the transaction and can be changed later.310 ///311 /// # Permissions312 ///313 /// * Anyone.314 ///315 /// # Arguments316 ///317 /// * collection_name: UTF-16 string with collection name (limit 64 characters), will be stored as zero-terminated.318 ///319 /// * collection_description: UTF-16 string with collection description (limit 256 characters), will be stored as zero-terminated.320 ///321 /// * token_prefix: UTF-8 string with token prefix.322 ///323 /// * mode: [CollectionMode] collection type and type dependent data.324 // returns collection ID325 #[weight = <SelfWeightOf<T>>::create_collection()]326 #[transactional]327 #[deprecated]328 pub fn create_collection(origin,329 collection_name: BoundedVec<u16, ConstU32<MAX_COLLECTION_NAME_LENGTH>>,330 collection_description: BoundedVec<u16, ConstU32<MAX_COLLECTION_DESCRIPTION_LENGTH>>,331 token_prefix: BoundedVec<u8, ConstU32<MAX_TOKEN_PREFIX_LENGTH>>,332 mode: CollectionMode) -> DispatchResult {333 let data: CreateCollectionData<T::AccountId> = CreateCollectionData {334 name: collection_name,335 description: collection_description,336 token_prefix,337 mode,338 ..Default::default()339 };340 Self::create_collection_ex(origin, data)341 }342343 /// This method creates a collection344 ///345 /// Prefer it to deprecated [`created_collection`] method346 #[weight = <SelfWeightOf<T>>::create_collection()]347 #[transactional]348 pub fn create_collection_ex(origin, data: CreateCollectionData<T::AccountId>) -> DispatchResult {349 let owner = ensure_signed(origin)?;350351 let _id = match data.mode {352 CollectionMode::NFT => {<PalletNonfungible<T>>::init_collection(owner, data)?},353 CollectionMode::Fungible(decimal_points) => {354 // check params355 ensure!(decimal_points <= MAX_DECIMAL_POINTS, Error::<T>::CollectionDecimalPointLimitExceeded);356 <PalletFungible<T>>::init_collection(owner, data)?357 }358 CollectionMode::ReFungible => {359 <PalletRefungible<T>>::init_collection(owner, data)?360 }361 };362363 Ok(())364 }365366 /// **DANGEROUS**: Destroys collection and all NFTs within this collection. Users irrecoverably lose their assets and may lose real money.367 ///368 /// # Permissions369 ///370 /// * Collection Owner.371 ///372 /// # Arguments373 ///374 /// * collection_id: collection to destroy.375 #[weight = <SelfWeightOf<T>>::destroy_collection()]376 #[transactional]377 pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {378 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);379380 let collection = <CollectionHandle<T>>::try_get(collection_id)?;381 collection.check_is_owner(&sender)?;382383 // =========384385 match collection.mode {386 CollectionMode::ReFungible => PalletRefungible::destroy_collection(RefungibleHandle::cast(collection), &sender)?,387 CollectionMode::Fungible(_) => PalletFungible::destroy_collection(FungibleHandle::cast(collection), &sender)?,388 CollectionMode::NFT => PalletNonfungible::destroy_collection(NonfungibleHandle::cast(collection), &sender)?,389 }390391 <NftTransferBasket<T>>::remove_prefix(collection_id, None);392 <FungibleTransferBasket<T>>::remove_prefix(collection_id, None);393 <ReFungibleTransferBasket<T>>::remove_prefix((collection_id,), None);394395 <VariableMetaDataBasket<T>>::remove_prefix(collection_id, None);396 <NftApproveBasket<T>>::remove_prefix(collection_id, None);397 <FungibleApproveBasket<T>>::remove_prefix(collection_id, None);398 <RefungibleApproveBasket<T>>::remove_prefix((collection_id,), None);399400 Ok(())401 }402403 /// Add an address to allow list.404 ///405 /// # Permissions406 ///407 /// * Collection Owner408 /// * Collection Admin409 ///410 /// # Arguments411 ///412 /// * collection_id.413 ///414 /// * address.415 #[weight = <SelfWeightOf<T>>::add_to_allow_list()]416 #[transactional]417 pub fn add_to_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{418419 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);420 let collection = <CollectionHandle<T>>::try_get(collection_id)?;421422 <PalletCommon<T>>::toggle_allowlist(423 &collection,424 &sender,425 &address,426 true,427 )?;428429 Self::deposit_event(Event::<T>::AllowListAddressAdded(430 collection_id,431 address432 ));433434 Ok(())435 }436437 /// Remove an address from allow list.438 ///439 /// # Permissions440 ///441 /// * Collection Owner442 /// * Collection Admin443 ///444 /// # Arguments445 ///446 /// * collection_id.447 ///448 /// * address.449 #[weight = <SelfWeightOf<T>>::remove_from_allow_list()]450 #[transactional]451 pub fn remove_from_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{452453 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);454 let collection = <CollectionHandle<T>>::try_get(collection_id)?;455456 <PalletCommon<T>>::toggle_allowlist(457 &collection,458 &sender,459 &address,460 false,461 )?;462463 <Pallet<T>>::deposit_event(Event::<T>::AllowListAddressRemoved(464 collection_id,465 address466 ));467468 Ok(())469 }470471 /// Toggle between normal and allow list access for the methods with access for `Anyone`.472 ///473 /// # Permissions474 ///475 /// * Collection Owner.476 ///477 /// # Arguments478 ///479 /// * collection_id.480 ///481 /// * mode: [AccessMode]482 #[weight = <SelfWeightOf<T>>::set_public_access_mode()]483 #[transactional]484 pub fn set_public_access_mode(origin, collection_id: CollectionId, mode: AccessMode) -> DispatchResult485 {486 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);487488 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;489 target_collection.check_is_owner(&sender)?;490491 target_collection.access = mode.clone();492493 <Pallet<T>>::deposit_event(Event::<T>::PublicAccessModeSet(494 collection_id,495 mode496 ));497498 target_collection.save()499 }500501 /// Allows Anyone to create tokens if:502 /// * Allow List is enabled, and503 /// * Address is added to allow list, and504 /// * This method was called with True parameter505 ///506 /// # Permissions507 /// * Collection Owner508 ///509 /// # Arguments510 ///511 /// * collection_id.512 ///513 /// * mint_permission: Boolean parameter. If True, allows minting to Anyone with conditions above.514 #[weight = <SelfWeightOf<T>>::set_mint_permission()]515 #[transactional]516 pub fn set_mint_permission(origin, collection_id: CollectionId, mint_permission: bool) -> DispatchResult517 {518 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);519520 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;521 target_collection.check_is_owner(&sender)?;522523 target_collection.mint_mode = mint_permission;524525 <Pallet<T>>::deposit_event(Event::<T>::MintPermissionSet(526 collection_id527 ));528529 target_collection.save()530 }531532 /// Change the owner of the collection.533 ///534 /// # Permissions535 ///536 /// * Collection Owner.537 ///538 /// # Arguments539 ///540 /// * collection_id.541 ///542 /// * new_owner.543 #[weight = <SelfWeightOf<T>>::change_collection_owner()]544 #[transactional]545 pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {546547 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);548549 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;550 target_collection.check_is_owner(&sender)?;551552 target_collection.owner = new_owner.clone();553 <Pallet<T>>::deposit_event(Event::<T>::CollectionOwnedChanged(554 collection_id,555 new_owner556 ));557558 target_collection.save()559 }560561 /// Adds an admin of the Collection.562 /// NFT Collection can be controlled by multiple admin addresses (some which can also be servers, for example). Admins can issue and burn NFTs, as well as add and remove other admins, but cannot change NFT or Collection ownership.563 ///564 /// # Permissions565 ///566 /// * Collection Owner.567 /// * Collection Admin.568 ///569 /// # Arguments570 ///571 /// * collection_id: ID of the Collection to add admin for.572 ///573 /// * new_admin_id: Address of new admin to add.574 #[weight = <SelfWeightOf<T>>::add_collection_admin()]575 #[transactional]576 pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::CrossAccountId) -> DispatchResult {577 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);578 let collection = <CollectionHandle<T>>::try_get(collection_id)?;579580 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminAdded(581 collection_id,582 new_admin_id.clone()583 ));584585 <PalletCommon<T>>::toggle_admin(&collection, &sender, &new_admin_id, true)586 }587588 /// Remove admin address of the Collection. An admin address can remove itself. List of admins may become empty, in which case only Collection Owner will be able to add an Admin.589 ///590 /// # Permissions591 ///592 /// * Collection Owner.593 /// * Collection Admin.594 ///595 /// # Arguments596 ///597 /// * collection_id: ID of the Collection to remove admin for.598 ///599 /// * account_id: Address of admin to remove.600 #[weight = <SelfWeightOf<T>>::remove_collection_admin()]601 #[transactional]602 pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::CrossAccountId) -> DispatchResult {603 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);604 let collection = <CollectionHandle<T>>::try_get(collection_id)?;605606 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminRemoved(607 collection_id,608 account_id.clone()609 ));610611 <PalletCommon<T>>::toggle_admin(&collection, &sender, &account_id, false)612 }613614 /// # Permissions615 ///616 /// * Collection Owner617 ///618 /// # Arguments619 ///620 /// * collection_id.621 ///622 /// * new_sponsor.623 #[weight = <SelfWeightOf<T>>::set_collection_sponsor()]624 #[transactional]625 pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {626 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);627628 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;629 target_collection.check_is_owner(&sender)?;630631 target_collection.sponsorship = SponsorshipState::Unconfirmed(new_sponsor.clone());632633 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(634 collection_id,635 new_sponsor636 ));637638 target_collection.save()639 }640641 /// # Permissions642 ///643 /// * Sponsor.644 ///645 /// # Arguments646 ///647 /// * collection_id.648 #[weight = <SelfWeightOf<T>>::confirm_sponsorship()]649 #[transactional]650 pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {651 let sender = ensure_signed(origin)?;652653 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;654 ensure!(655 target_collection.sponsorship.pending_sponsor() == Some(&sender),656 Error::<T>::ConfirmUnsetSponsorFail657 );658659 target_collection.sponsorship = SponsorshipState::Confirmed(sender.clone());660661 <Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(662 collection_id,663 sender664 ));665666 target_collection.save()667 }668669 /// Switch back to pay-per-own-transaction model.670 ///671 /// # Permissions672 ///673 /// * Collection owner.674 ///675 /// # Arguments676 ///677 /// * collection_id.678 #[weight = <SelfWeightOf<T>>::remove_collection_sponsor()]679 #[transactional]680 pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {681 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);682683 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;684 target_collection.check_is_owner(&sender)?;685686 target_collection.sponsorship = SponsorshipState::Disabled;687688 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(689 collection_id690 ));691 target_collection.save()692 }693694 /// This method creates a concrete instance of NFT Collection created with CreateCollection method.695 ///696 /// # Permissions697 ///698 /// * Collection Owner.699 /// * Collection Admin.700 /// * Anyone if701 /// * Allow List is enabled, and702 /// * Address is added to allow list, and703 /// * MintPermission is enabled (see SetMintPermission method)704 ///705 /// # Arguments706 ///707 /// * collection_id: ID of the collection.708 ///709 /// * owner: Address, initial owner of the NFT.710 ///711 /// * data: Token data to store on chain.712 #[weight = <CommonWeights<T>>::create_item()]713 #[transactional]714 pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResultWithPostInfo {715 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);716717 dispatch_call::<T, _>(collection_id, |d| d.create_item(sender, owner, data))718 }719720 /// This method creates multiple items in a collection created with CreateCollection method.721 ///722 /// # Permissions723 ///724 /// * Collection Owner.725 /// * Collection Admin.726 /// * Anyone if727 /// * Allow List is enabled, and728 /// * Address is added to allow list, and729 /// * MintPermission is enabled (see SetMintPermission method)730 ///731 /// # Arguments732 ///733 /// * collection_id: ID of the collection.734 ///735 /// * itemsData: Array items properties. Each property is an array of bytes itself, see [create_item].736 ///737 /// * owner: Address, initial owner of the NFT.738 #[weight = <CommonWeights<T>>::create_multiple_items(items_data.len() as u32)]739 #[transactional]740 pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResultWithPostInfo {741 ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);742 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);743744 dispatch_call::<T, _>(collection_id, |d| d.create_multiple_items(sender, owner, items_data))745 }746747 #[weight = <CommonWeights<T>>::create_multiple_items_ex(&data)]748 #[transactional]749 pub fn create_multiple_items_ex(origin, collection_id: CollectionId, data: CreateItemExData<T::CrossAccountId>) -> DispatchResultWithPostInfo {750 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);751752 dispatch_call::<T, _>(collection_id, |d| d.create_multiple_items_ex(sender, data))753 }754755 // TODO! transaction weight756757 /// Set transfers_enabled value for particular collection758 ///759 /// # Permissions760 ///761 /// * Collection Owner.762 ///763 /// # Arguments764 ///765 /// * collection_id: ID of the collection.766 ///767 /// * value: New flag value.768 #[weight = <SelfWeightOf<T>>::set_transfers_enabled_flag()]769 #[transactional]770 pub fn set_transfers_enabled_flag(origin, collection_id: CollectionId, value: bool) -> DispatchResult {771 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);772 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;773 target_collection.check_is_owner(&sender)?;774775 // =========776777 target_collection.limits.transfers_enabled = Some(value);778 target_collection.save()779 }780781 /// Destroys a concrete instance of NFT.782 ///783 /// # Permissions784 ///785 /// * Collection Owner.786 /// * Collection Admin.787 /// * Current NFT Owner.788 ///789 /// # Arguments790 ///791 /// * collection_id: ID of the collection.792 ///793 /// * item_id: ID of NFT to burn.794 #[weight = <CommonWeights<T>>::burn_item()]795 #[transactional]796 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {797 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);798799 let post_info = dispatch_call::<T, _>(collection_id, |d| d.burn_item(sender, item_id, value))?;800 if value == 1 {801 <NftTransferBasket<T>>::remove(collection_id, item_id);802 <NftApproveBasket<T>>::remove(collection_id, item_id);803 }804 // Those maps should be cleared only if token disappears completly, need to move this part of logic to pallets?805 // <FungibleApproveBasket<T>>::remove(collection_id, sender.as_sub());806 // <RefungibleApproveBasket<T>>::remove((collection_id, item_id, sender.as_sub()));807 Ok(post_info)808 }809810 /// Destroys a concrete instance of NFT on behalf of the owner811 /// See also: [`approve`]812 ///813 /// # Permissions814 ///815 /// * Collection Owner.816 /// * Collection Admin.817 /// * Current NFT Owner.818 ///819 /// # Arguments820 ///821 /// * collection_id: ID of the collection.822 ///823 /// * item_id: ID of NFT to burn.824 ///825 /// * from: owner of item826 #[weight = <CommonWeights<T>>::burn_from()]827 #[transactional]828 pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {829 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);830831 dispatch_call::<T, _>(collection_id, |d| d.burn_from(sender, from, item_id, value))832 }833834 /// Change ownership of the token.835 ///836 /// # Permissions837 ///838 /// * Collection Owner839 /// * Collection Admin840 /// * Current NFT owner841 ///842 /// # Arguments843 ///844 /// * recipient: Address of token recipient.845 ///846 /// * collection_id.847 ///848 /// * item_id: ID of the item849 /// * Non-Fungible Mode: Required.850 /// * Fungible Mode: Ignored.851 /// * Re-Fungible Mode: Required.852 ///853 /// * value: Amount to transfer.854 /// * Non-Fungible Mode: Ignored855 /// * Fungible Mode: Must specify transferred amount856 /// * Re-Fungible Mode: Must specify transferred portion (between 0 and 1)857 #[weight = <CommonWeights<T>>::transfer()]858 #[transactional]859 pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {860 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);861862 dispatch_call::<T, _>(collection_id, |d| d.transfer(sender, recipient, item_id, value))863 }864865 /// Set, change, or remove approved address to transfer the ownership of the NFT.866 ///867 /// # Permissions868 ///869 /// * Collection Owner870 /// * Collection Admin871 /// * Current NFT owner872 ///873 /// # Arguments874 ///875 /// * approved: Address that is approved to transfer this NFT or zero (if needed to remove approval).876 ///877 /// * collection_id.878 ///879 /// * item_id: ID of the item.880 #[weight = <CommonWeights<T>>::approve()]881 #[transactional]882 pub fn approve(origin, spender: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResultWithPostInfo {883 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);884885 dispatch_call::<T, _>(collection_id, |d| d.approve(sender, spender, item_id, amount))886 }887888 /// Change ownership of a NFT on behalf of the owner. See Approve method for additional information. After this method executes, the approval is removed so that the approved address will not be able to transfer this NFT again from this owner.889 ///890 /// # Permissions891 /// * Collection Owner892 /// * Collection Admin893 /// * Current NFT owner894 /// * Address approved by current NFT owner895 ///896 /// # Arguments897 ///898 /// * from: Address that owns token.899 ///900 /// * recipient: Address of token recipient.901 ///902 /// * collection_id.903 ///904 /// * item_id: ID of the item.905 ///906 /// * value: Amount to transfer.907 #[weight = <CommonWeights<T>>::transfer_from()]908 #[transactional]909 pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo {910 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);911912 dispatch_call::<T, _>(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value))913 }914915 /// Set off-chain data schema.916 ///917 /// # Permissions918 ///919 /// * Collection Owner920 /// * Collection Admin921 ///922 /// # Arguments923 ///924 /// * collection_id.925 ///926 /// * schema: String representing the offchain data schema.927 #[weight = <CommonWeights<T>>::set_variable_metadata(data.len() as u32)]928 #[transactional]929 pub fn set_variable_meta_data (930 origin,931 collection_id: CollectionId,932 item_id: TokenId,933 data: BoundedVec<u8, CustomDataLimit>,934 ) -> DispatchResultWithPostInfo {935 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);936937 dispatch_call::<T, _>(collection_id, |d| d.set_variable_metadata(sender, item_id, data))938 }939940 /// Set meta_update_permission value for particular collection941 ///942 /// # Permissions943 ///944 /// * Collection Owner.945 ///946 /// # Arguments947 ///948 /// * collection_id: ID of the collection.949 ///950 /// * value: New flag value.951 #[weight = <SelfWeightOf<T>>::set_meta_update_permission_flag()]952 #[transactional]953 pub fn set_meta_update_permission_flag(origin, collection_id: CollectionId, value: MetaUpdatePermission) -> DispatchResult {954 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);955 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;956957 ensure!(958 target_collection.meta_update_permission != MetaUpdatePermission::None,959 <CommonError<T>>::MetadataFlagFrozen,960 );961 target_collection.check_is_owner(&sender)?;962963 target_collection.meta_update_permission = value;964965 target_collection.save()966 }967968 /// Set schema standard969 /// ImageURL970 /// Unique971 ///972 /// # Permissions973 ///974 /// * Collection Owner975 /// * Collection Admin976 ///977 /// # Arguments978 ///979 /// * collection_id.980 ///981 /// * schema: SchemaVersion: enum982 #[weight = <SelfWeightOf<T>>::set_schema_version()]983 #[transactional]984 pub fn set_schema_version(985 origin,986 collection_id: CollectionId,987 version: SchemaVersion988 ) -> DispatchResult {989 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);990 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;991 target_collection.check_is_owner_or_admin(&sender)?;992 target_collection.schema_version = version;993994 <Pallet<T>>::deposit_event(Event::<T>::SchemaVersionSet(995 collection_id996 ));997998 target_collection.save()999 }10001001 /// Set off-chain data schema.1002 ///1003 /// # Permissions1004 ///1005 /// * Collection Owner1006 /// * Collection Admin1007 ///1008 /// # Arguments1009 ///1010 /// * collection_id.1011 ///1012 /// * schema: String representing the offchain data schema.1013 #[weight = <SelfWeightOf<T>>::set_offchain_schema(schema.len() as u32)]1014 #[transactional]1015 pub fn set_offchain_schema(1016 origin,1017 collection_id: CollectionId,1018 schema: BoundedVec<u8, ConstU32<OFFCHAIN_SCHEMA_LIMIT>>,1019 ) -> DispatchResult {1020 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1021 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1022 target_collection.check_is_owner_or_admin(&sender)?;10231024 target_collection.offchain_schema = schema;10251026 <Pallet<T>>::deposit_event(Event::<T>::OffchainSchemaSet(1027 collection_id1028 ));10291030 target_collection.save()1031 }10321033 /// Set const on-chain data schema.1034 ///1035 /// # Permissions1036 ///1037 /// * Collection Owner1038 /// * Collection Admin1039 ///1040 /// # Arguments1041 ///1042 /// * collection_id.1043 ///1044 /// * schema: String representing the const on-chain data schema.1045 #[weight = <SelfWeightOf<T>>::set_const_on_chain_schema(schema.len() as u32)]1046 #[transactional]1047 pub fn set_const_on_chain_schema (1048 origin,1049 collection_id: CollectionId,1050 schema: BoundedVec<u8, ConstU32<CONST_ON_CHAIN_SCHEMA_LIMIT>>1051 ) -> DispatchResult {1052 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1053 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1054 target_collection.check_is_owner_or_admin(&sender)?;10551056 target_collection.const_on_chain_schema = schema;10571058 <Pallet<T>>::deposit_event(Event::<T>::ConstOnChainSchemaSet(1059 collection_id1060 ));10611062 target_collection.save()1063 }10641065 /// Set variable on-chain data schema.1066 ///1067 /// # Permissions1068 ///1069 /// * Collection Owner1070 /// * Collection Admin1071 ///1072 /// # Arguments1073 ///1074 /// * collection_id.1075 ///1076 /// * schema: String representing the variable on-chain data schema.1077 #[weight = <SelfWeightOf<T>>::set_const_on_chain_schema(schema.len() as u32)]1078 #[transactional]1079 pub fn set_variable_on_chain_schema (1080 origin,1081 collection_id: CollectionId,1082 schema: BoundedVec<u8, ConstU32<VARIABLE_ON_CHAIN_SCHEMA_LIMIT>>1083 ) -> DispatchResult {1084 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1085 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1086 target_collection.check_is_owner_or_admin(&sender)?;10871088 target_collection.variable_on_chain_schema = schema;10891090 <Pallet<T>>::deposit_event(Event::<T>::VariableOnChainSchemaSet(1091 collection_id1092 ));10931094 target_collection.save()1095 }10961097 #[weight = <SelfWeightOf<T>>::set_collection_limits()]1098 #[transactional]1099 pub fn set_collection_limits(1100 origin,1101 collection_id: CollectionId,1102 new_limit: CollectionLimits,1103 ) -> DispatchResult {1104 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1105 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1106 target_collection.check_is_owner(&sender)?;1107 let old_limit = &target_collection.limits;11081109 target_collection.limits = <PalletCommon<T>>::clamp_limits(target_collection.mode.clone(), &old_limit, new_limit)?;11101111 <Pallet<T>>::deposit_event(Event::<T>::CollectionLimitSet(1112 collection_id1113 ));11141115 target_collection.save()1116 }1117 }1118}pallets/unique/src/sponsorship.rsdiffbeforeafterboth--- a/pallets/unique/src/sponsorship.rs
+++ b/pallets/unique/src/sponsorship.rs
@@ -29,6 +29,7 @@
CollectionId, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, MetaUpdatePermission,
NFT_SPONSOR_TRANSFER_TIMEOUT, REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, TokenId,
};
+use sp_runtime::traits::Saturating;
use pallet_common::{CollectionHandle};
use pallet_evm::account::CrossAccountId;
@@ -301,3 +302,65 @@
}
}
}
+
+use crate::SponsorshipPredict;
+use up_data_structs::SponsorshipState;
+pub struct UniqueSponsorshipPredict<T>(PhantomData<T>);
+
+impl<T> SponsorshipPredict<T> for UniqueSponsorshipPredict<T>
+where
+ T: Config,
+{
+ fn predict(collection_id: CollectionId, who: T::CrossAccountId, token: TokenId) -> Option<u64>
+ where
+ u64: From<<T as frame_system::Config>::BlockNumber>,
+ {
+ let collection = <CollectionHandle<T>>::try_get(collection_id).ok()?;
+ let _ = collection.sponsorship.sponsor()?;
+
+ // sponsor timeout
+ let block_number = <frame_system::Pallet<T>>::block_number() as T::BlockNumber;
+ let limit = collection
+ .limits
+ .sponsor_transfer_timeout(match collection.mode {
+ CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,
+ CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,
+ CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,
+ });
+
+ let last_tx_block = match collection.mode {
+ CollectionMode::NFT => <NftTransferBasket<T>>::get(collection.id, token),
+ CollectionMode::Fungible(_) => {
+ <FungibleTransferBasket<T>>::get(collection.id, who.as_sub())
+ }
+ CollectionMode::ReFungible => {
+ <ReFungibleTransferBasket<T>>::get((collection.id, token, who.as_sub()))
+ }
+ };
+
+ if let Some(last_tx_block) = last_tx_block {
+ return Some(
+ last_tx_block
+ .saturating_add(limit.into())
+ .saturating_sub(block_number)
+ .into(),
+ );
+ }
+
+ let token_exists = match collection.mode {
+ CollectionMode::NFT => {
+ <pallet_nonfungible::TokenData<T>>::contains_key((collection.id, token))
+ }
+ CollectionMode::Fungible(_) => true,
+ CollectionMode::ReFungible => {
+ <pallet_refungible::TotalSupply<T>>::contains_key((collection.id, token))
+ }
+ };
+
+ if token_exists {
+ Some(0)
+ } else {
+ None
+ }
+ }
+}
primitives/rpc/src/lib.rsdiffbeforeafterboth--- a/primitives/rpc/src/lib.rs
+++ b/primitives/rpc/src/lib.rs
@@ -59,6 +59,7 @@
fn last_token_id(collection: CollectionId) -> Result<TokenId>;
fn collection_by_id(collection: CollectionId) -> Result<Option<Collection<AccountId>>>;
fn collection_stats() -> Result<CollectionStats>;
+ fn next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Result<Option<u64>>;
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,13 @@
fn collection_stats() -> Result<CollectionStats, DispatchError> {
Ok(<pallet_common::Pallet<Runtime>>::collection_stats())
}
+ fn next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Result<Option<u64>, DispatchError> {
+ Ok(<pallet_unique::UniqueSponsorshipPredict<Runtime> as
+ pallet_unique::SponsorshipPredict<Runtime>>::predict(
+ collection,
+ account,
+ token))
+ }
fn effective_collection_limits(collection: CollectionId) -> Result<Option<CollectionLimits>, DispatchError> {
Ok(<pallet_common::Pallet<Runtime>>::effective_collection_limits(collection))
tests/src/interfaces/augment-api-rpc.tsdiffbeforeafterboth--- a/tests/src/interfaces/augment-api-rpc.ts
+++ b/tests/src/interfaces/augment-api-rpc.ts
@@ -623,6 +623,10 @@
* Get token variable metadata
**/
variableMetadata: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<Bytes>>;
+ /**
+ * nextSponsored transaction
+ **/
+ nextSponsored: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, account: AccountId | string | Uint8Array | { Substrate: any } | { Ethereum: any } | string | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<Option<u64>>>;
};
web3: {
/**
tests/src/interfaces/unique/definitions.tsdiffbeforeafterboth--- a/tests/src/interfaces/unique/definitions.ts
+++ b/tests/src/interfaces/unique/definitions.ts
@@ -55,6 +55,7 @@
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'),
+ nextSponsored: fun('Get number of blocks when sponsored transaction is available', [collectionParam, crossAccountParam(), tokenParam], 'Option<u64>'),
effectiveCollectionLimits: fun('Get effective collection limits', [collectionParam], 'Option<UpDataStructsCollectionLimits>'),
},
};
tests/src/nextSponsoring.test.tsdiffbeforeafterboth--- /dev/null
+++ b/tests/src/nextSponsoring.test.ts
@@ -0,0 +1,110 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+import {ApiPromise} from '@polkadot/api';
+import {IKeyringPair} from '@polkadot/types/types';
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import privateKey from './substrate/privateKey';
+import {default as usingApi} from './substrate/substrate-api';
+import {
+ createCollectionExpectSuccess,
+ setCollectionSponsorExpectSuccess,
+ confirmSponsorshipExpectSuccess,
+ createItemExpectSuccess,
+ transferExpectSuccess,
+ normalizeAccountId,
+ getNextSponsored,
+} from './util/helpers';
+
+chai.use(chaiAsPromised);
+const expect = chai.expect;
+
+
+
+describe('Integration Test getNextSponsored(collection_id, owner, item_id):', () => {
+ let alice: IKeyringPair;
+ let bob: IKeyringPair;
+
+ before(async () => {
+ await usingApi(async () => {
+ alice = privateKey('//Alice');
+ bob = privateKey('//Bob');
+ });
+ });
+
+ it('NFT', async () => {
+ await usingApi(async (api: ApiPromise) => {
+
+ // Not existing collection
+ expect(await getNextSponsored(api, 0, normalizeAccountId(alice), 0)).to.be.equal(-1);
+
+ const collectionId = await createCollectionExpectSuccess();
+ const itemId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);
+
+ // Check with Disabled sponsoring state
+ expect(await getNextSponsored(api, collectionId, normalizeAccountId(alice), itemId)).to.be.equal(-1);
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+
+ // Check with Unconfirmed sponsoring state
+ expect(await getNextSponsored(api, collectionId, normalizeAccountId(alice), itemId)).to.be.equal(-1);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ // Check with Confirmed sponsoring state
+ expect(await getNextSponsored(api, collectionId, normalizeAccountId(alice), itemId)).to.be.equal(0);
+
+ // After transfer
+ await transferExpectSuccess(collectionId, itemId, alice, bob, 1);
+ expect(await getNextSponsored(api, collectionId, normalizeAccountId(alice), itemId)).to.be.equal(5);
+
+ // Not existing token
+ expect(await getNextSponsored(api, collectionId, normalizeAccountId(alice), itemId+1)).to.be.equal(-1);
+ });
+ });
+
+ it('Fungible', async () => {
+ await usingApi(async (api: ApiPromise) => {
+
+ const createMode = 'Fungible';
+ const funCollectionId = await createCollectionExpectSuccess({mode: {type: createMode, decimalPoints: 0}});
+ await createItemExpectSuccess(alice, funCollectionId, createMode);
+ await setCollectionSponsorExpectSuccess(funCollectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(funCollectionId, '//Bob');
+ expect(await getNextSponsored(api, funCollectionId, normalizeAccountId(alice), 0)).to.be.equal(0);
+
+ await transferExpectSuccess(funCollectionId, 0, alice, bob, 10, 'Fungible');
+ expect(await getNextSponsored(api, funCollectionId, normalizeAccountId(alice), 0)).to.be.equal(5);
+ });
+ });
+
+ it('ReFungible', async () => {
+ await usingApi(async (api: ApiPromise) => {
+
+ const createMode = 'ReFungible';
+ const refunCollectionId = await createCollectionExpectSuccess({mode: {type: createMode}});
+ const refunItemId = await createItemExpectSuccess(alice, refunCollectionId, createMode);
+ await setCollectionSponsorExpectSuccess(refunCollectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(refunCollectionId, '//Bob');
+ expect(await getNextSponsored(api, refunCollectionId, normalizeAccountId(alice), refunItemId)).to.be.equal(0);
+
+ await transferExpectSuccess(refunCollectionId, refunItemId, alice, bob, 10, 'ReFungible');
+ expect(await getNextSponsored(api, refunCollectionId, normalizeAccountId(alice), refunItemId)).to.be.equal(5);
+
+ // Not existing token
+ expect(await getNextSponsored(api, refunCollectionId, normalizeAccountId(alice), refunItemId+1)).to.be.equal(-1);
+ });
+ });
+});
tests/src/util/helpers.tsdiffbeforeafterboth--- a/tests/src/util/helpers.ts
+++ b/tests/src/util/helpers.ts
@@ -608,6 +608,15 @@
});
}
+export async function getNextSponsored(
+ api: ApiPromise,
+ collectionId: number,
+ account: string | CrossAccountId,
+ tokenId: number,
+): Promise<number> {
+ return Number((await api.rpc.unique.nextSponsored(collectionId, account, tokenId)).unwrapOr(-1));
+}
+
export async function toggleContractAllowlistExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, value = true) {
await usingApi(async (api) => {
const tx = api.tx.unique.toggleContractAllowList(contractAddress, value);