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, UniqueSponsorshipPredict};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;8485pub trait SponsorshipPredict<T: Config> {86 fn predict(collection: CollectionId, account: T::CrossAccountId, token: TokenId) -> Option<u64>87 where88 u64: From<<T as frame_system::Config>::BlockNumber>;89}9091decl_error! {92 /// Error for non-fungible-token module.93 pub enum Error for Module<T: Config> {94 /// Decimal_points parameter must be lower than MAX_DECIMAL_POINTS constant, currently it is 30.95 CollectionDecimalPointLimitExceeded,96 /// This address is not set as sponsor, use setCollectionSponsor first.97 ConfirmUnsetSponsorFail,98 /// Length of items properties must be greater than 0.99 EmptyArgument,100 }101}102103pub trait Config:104 system::Config105 + pallet_evm_coder_substrate::Config106 + pallet_common::Config107 + pallet_nonfungible::Config108 + pallet_refungible::Config109 + pallet_fungible::Config110 + Sized111 + TypeInfo112{113 type Event: From<Event<Self>> + Into<<Self as frame_system::Config>::Event>;114115 /// Weight information for extrinsics in this pallet.116 type WeightInfo: WeightInfo;117}118119decl_event! {120 pub enum Event<T>121 where122 <T as frame_system::Config>::AccountId,123 <T as pallet_evm::account::Config>::CrossAccountId,124 {125 /// Collection sponsor was removed126 ///127 /// # Arguments128 ///129 /// * collection_id: Globally unique collection identifier.130 CollectionSponsorRemoved(CollectionId),131132 /// Collection admin was added133 ///134 /// # Arguments135 ///136 /// * collection_id: Globally unique collection identifier.137 ///138 /// * admin: Admin address.139 CollectionAdminAdded(CollectionId, CrossAccountId),140141 /// Collection owned was change142 ///143 /// # Arguments144 ///145 /// * collection_id: Globally unique collection identifier.146 ///147 /// * owner: New owner address.148 CollectionOwnedChanged(CollectionId, AccountId),149150 /// Collection sponsor was set151 ///152 /// # Arguments153 ///154 /// * collection_id: Globally unique collection identifier.155 ///156 /// * owner: New sponsor address.157 CollectionSponsorSet(CollectionId, AccountId),158159 /// const on chain schema was set160 ///161 /// # Arguments162 ///163 /// * collection_id: Globally unique collection identifier.164 ConstOnChainSchemaSet(CollectionId),165166 /// New sponsor was confirm167 ///168 /// # Arguments169 ///170 /// * collection_id: Globally unique collection identifier.171 ///172 /// * sponsor: New sponsor address.173 SponsorshipConfirmed(CollectionId, AccountId),174175 /// Collection admin was removed176 ///177 /// # Arguments178 ///179 /// * collection_id: Globally unique collection identifier.180 ///181 /// * admin: Admin address.182 CollectionAdminRemoved(CollectionId, CrossAccountId),183184 /// Address was remove from allow list185 ///186 /// # Arguments187 ///188 /// * collection_id: Globally unique collection identifier.189 ///190 /// * user: Address.191 AllowListAddressRemoved(CollectionId, CrossAccountId),192193 /// Address was add to allow list194 ///195 /// # Arguments196 ///197 /// * collection_id: Globally unique collection identifier.198 ///199 /// * user: Address.200 AllowListAddressAdded(CollectionId, CrossAccountId),201202 /// Collection limits was set203 ///204 /// # Arguments205 ///206 /// * collection_id: Globally unique collection identifier.207 CollectionLimitSet(CollectionId),208209 /// Mint permission was set210 ///211 /// # Arguments212 ///213 /// * collection_id: Globally unique collection identifier.214 MintPermissionSet(CollectionId),215216 /// Offchain schema was set217 ///218 /// # Arguments219 ///220 /// * collection_id: Globally unique collection identifier.221 OffchainSchemaSet(CollectionId),222223 /// Public access mode was set224 ///225 /// # Arguments226 ///227 /// * collection_id: Globally unique collection identifier.228 ///229 /// * mode: New access state.230 PublicAccessModeSet(CollectionId, AccessMode),231232 /// Schema version was set233 ///234 /// # Arguments235 ///236 /// * collection_id: Globally unique collection identifier.237 SchemaVersionSet(CollectionId),238239 /// Variable on chain schema was set240 ///241 /// # Arguments242 ///243 /// * collection_id: Globally unique collection identifier.244 VariableOnChainSchemaSet(CollectionId),245 }246}247248type SelfWeightOf<T> = <T as Config>::WeightInfo;249250// # Used definitions251//252// ## User control levels253//254// chain-controlled - key is uncontrolled by user255// i.e autoincrementing index256// can use non-cryptographic hash257// real - key is controlled by user258// but it is hard to generate enough colliding values, i.e owner of signed txs259// can use non-cryptographic hash260// controlled - key is completly controlled by users261// i.e maps with mutable keys262// should use cryptographic hash263//264// ## User control level downgrade reasons265//266// ?1 - chain-controlled -> controlled267// collections/tokens can be destroyed, resulting in massive holes268// ?2 - chain-controlled -> controlled269// same as ?1, but can be only added, resulting in easier exploitation270// ?3 - real -> controlled271// no confirmation required, so addresses can be easily generated272decl_storage! {273 trait Store for Module<T: Config> as Unique {274275 //#region Private members276 /// Used for migrations277 ChainVersion: u64;278 //#endregion279280 //#region Tokens transfer rate limit baskets281 /// (Collection id (controlled?2), who created (real))282 /// TODO: Off chain worker should remove from this map when collection gets removed283 pub CreateItemBasket get(fn create_item_basket): map hasher(blake2_128_concat) (CollectionId, T::AccountId) => Option<T::BlockNumber>;284 /// Collection id (controlled?2), token id (controlled?2)285 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;286 /// Collection id (controlled?2), owning user (real)287 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;288 /// Collection id (controlled?2), token id (controlled?2)289 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>;290 //#endregion291292 /// Variable metadata sponsoring293 /// Collection id (controlled?2), token id (controlled?2)294 pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;295 /// Approval sponsoring296 pub NftApproveBasket get(fn nft_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;297 pub FungibleApproveBasket get(fn fungible_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;298 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>;299 }300}301302decl_module! {303 pub struct Module<T: Config> for enum Call304 where305 origin: T::Origin306 {307 type Error = Error<T>;308309 fn deposit_event() = default;310311 fn on_initialize(_now: T::BlockNumber) -> Weight {312 0313 }314315 /// 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.316 ///317 /// # Permissions318 ///319 /// * Anyone.320 ///321 /// # Arguments322 ///323 /// * collection_name: UTF-16 string with collection name (limit 64 characters), will be stored as zero-terminated.324 ///325 /// * collection_description: UTF-16 string with collection description (limit 256 characters), will be stored as zero-terminated.326 ///327 /// * token_prefix: UTF-8 string with token prefix.328 ///329 /// * mode: [CollectionMode] collection type and type dependent data.330 // returns collection ID331 #[weight = <SelfWeightOf<T>>::create_collection()]332 #[transactional]333 #[deprecated]334 pub fn create_collection(origin,335 collection_name: BoundedVec<u16, ConstU32<MAX_COLLECTION_NAME_LENGTH>>,336 collection_description: BoundedVec<u16, ConstU32<MAX_COLLECTION_DESCRIPTION_LENGTH>>,337 token_prefix: BoundedVec<u8, ConstU32<MAX_TOKEN_PREFIX_LENGTH>>,338 mode: CollectionMode) -> DispatchResult {339 let data: CreateCollectionData<T::AccountId> = CreateCollectionData {340 name: collection_name,341 description: collection_description,342 token_prefix,343 mode,344 ..Default::default()345 };346 Self::create_collection_ex(origin, data)347 }348349 /// This method creates a collection350 ///351 /// Prefer it to deprecated [`created_collection`] method352 #[weight = <SelfWeightOf<T>>::create_collection()]353 #[transactional]354 pub fn create_collection_ex(origin, data: CreateCollectionData<T::AccountId>) -> DispatchResult {355 let owner = ensure_signed(origin)?;356357 let _id = match data.mode {358 CollectionMode::NFT => {<PalletNonfungible<T>>::init_collection(owner, data)?},359 CollectionMode::Fungible(decimal_points) => {360 // check params361 ensure!(decimal_points <= MAX_DECIMAL_POINTS, Error::<T>::CollectionDecimalPointLimitExceeded);362 <PalletFungible<T>>::init_collection(owner, data)?363 }364 CollectionMode::ReFungible => {365 <PalletRefungible<T>>::init_collection(owner, data)?366 }367 };368369 Ok(())370 }371372 /// **DANGEROUS**: Destroys collection and all NFTs within this collection. Users irrecoverably lose their assets and may lose real money.373 ///374 /// # Permissions375 ///376 /// * Collection Owner.377 ///378 /// # Arguments379 ///380 /// * collection_id: collection to destroy.381 #[weight = <SelfWeightOf<T>>::destroy_collection()]382 #[transactional]383 pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {384 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);385386 let collection = <CollectionHandle<T>>::try_get(collection_id)?;387 collection.check_is_owner(&sender)?;388389 // =========390391 match collection.mode {392 CollectionMode::ReFungible => PalletRefungible::destroy_collection(RefungibleHandle::cast(collection), &sender)?,393 CollectionMode::Fungible(_) => PalletFungible::destroy_collection(FungibleHandle::cast(collection), &sender)?,394 CollectionMode::NFT => PalletNonfungible::destroy_collection(NonfungibleHandle::cast(collection), &sender)?,395 }396397 <NftTransferBasket<T>>::remove_prefix(collection_id, None);398 <FungibleTransferBasket<T>>::remove_prefix(collection_id, None);399 <ReFungibleTransferBasket<T>>::remove_prefix((collection_id,), None);400401 <VariableMetaDataBasket<T>>::remove_prefix(collection_id, None);402 <NftApproveBasket<T>>::remove_prefix(collection_id, None);403 <FungibleApproveBasket<T>>::remove_prefix(collection_id, None);404 <RefungibleApproveBasket<T>>::remove_prefix((collection_id,), None);405406 Ok(())407 }408409 /// Add an address to allow list.410 ///411 /// # Permissions412 ///413 /// * Collection Owner414 /// * Collection Admin415 ///416 /// # Arguments417 ///418 /// * collection_id.419 ///420 /// * address.421 #[weight = <SelfWeightOf<T>>::add_to_allow_list()]422 #[transactional]423 pub fn add_to_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{424425 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);426 let collection = <CollectionHandle<T>>::try_get(collection_id)?;427428 <PalletCommon<T>>::toggle_allowlist(429 &collection,430 &sender,431 &address,432 true,433 )?;434435 Self::deposit_event(Event::<T>::AllowListAddressAdded(436 collection_id,437 address438 ));439440 Ok(())441 }442443 /// Remove an address from allow list.444 ///445 /// # Permissions446 ///447 /// * Collection Owner448 /// * Collection Admin449 ///450 /// # Arguments451 ///452 /// * collection_id.453 ///454 /// * address.455 #[weight = <SelfWeightOf<T>>::remove_from_allow_list()]456 #[transactional]457 pub fn remove_from_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{458459 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);460 let collection = <CollectionHandle<T>>::try_get(collection_id)?;461462 <PalletCommon<T>>::toggle_allowlist(463 &collection,464 &sender,465 &address,466 false,467 )?;468469 <Pallet<T>>::deposit_event(Event::<T>::AllowListAddressRemoved(470 collection_id,471 address472 ));473474 Ok(())475 }476477 /// Toggle between normal and allow list access for the methods with access for `Anyone`.478 ///479 /// # Permissions480 ///481 /// * Collection Owner.482 ///483 /// # Arguments484 ///485 /// * collection_id.486 ///487 /// * mode: [AccessMode]488 #[weight = <SelfWeightOf<T>>::set_public_access_mode()]489 #[transactional]490 pub fn set_public_access_mode(origin, collection_id: CollectionId, mode: AccessMode) -> DispatchResult491 {492 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);493494 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;495 target_collection.check_is_owner(&sender)?;496497 target_collection.access = mode.clone();498499 <Pallet<T>>::deposit_event(Event::<T>::PublicAccessModeSet(500 collection_id,501 mode502 ));503504 target_collection.save()505 }506507 /// Allows Anyone to create tokens if:508 /// * Allow List is enabled, and509 /// * Address is added to allow list, and510 /// * This method was called with True parameter511 ///512 /// # Permissions513 /// * Collection Owner514 ///515 /// # Arguments516 ///517 /// * collection_id.518 ///519 /// * mint_permission: Boolean parameter. If True, allows minting to Anyone with conditions above.520 #[weight = <SelfWeightOf<T>>::set_mint_permission()]521 #[transactional]522 pub fn set_mint_permission(origin, collection_id: CollectionId, mint_permission: bool) -> DispatchResult523 {524 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);525526 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;527 target_collection.check_is_owner(&sender)?;528529 target_collection.mint_mode = mint_permission;530531 <Pallet<T>>::deposit_event(Event::<T>::MintPermissionSet(532 collection_id533 ));534535 target_collection.save()536 }537538 /// Change the owner of the collection.539 ///540 /// # Permissions541 ///542 /// * Collection Owner.543 ///544 /// # Arguments545 ///546 /// * collection_id.547 ///548 /// * new_owner.549 #[weight = <SelfWeightOf<T>>::change_collection_owner()]550 #[transactional]551 pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {552553 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);554555 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;556 target_collection.check_is_owner(&sender)?;557558 target_collection.owner = new_owner.clone();559 <Pallet<T>>::deposit_event(Event::<T>::CollectionOwnedChanged(560 collection_id,561 new_owner562 ));563564 target_collection.save()565 }566567 /// Adds an admin of the Collection.568 /// 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.569 ///570 /// # Permissions571 ///572 /// * Collection Owner.573 /// * Collection Admin.574 ///575 /// # Arguments576 ///577 /// * collection_id: ID of the Collection to add admin for.578 ///579 /// * new_admin_id: Address of new admin to add.580 #[weight = <SelfWeightOf<T>>::add_collection_admin()]581 #[transactional]582 pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::CrossAccountId) -> DispatchResult {583 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);584 let collection = <CollectionHandle<T>>::try_get(collection_id)?;585586 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminAdded(587 collection_id,588 new_admin_id.clone()589 ));590591 <PalletCommon<T>>::toggle_admin(&collection, &sender, &new_admin_id, true)592 }593594 /// 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.595 ///596 /// # Permissions597 ///598 /// * Collection Owner.599 /// * Collection Admin.600 ///601 /// # Arguments602 ///603 /// * collection_id: ID of the Collection to remove admin for.604 ///605 /// * account_id: Address of admin to remove.606 #[weight = <SelfWeightOf<T>>::remove_collection_admin()]607 #[transactional]608 pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::CrossAccountId) -> DispatchResult {609 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);610 let collection = <CollectionHandle<T>>::try_get(collection_id)?;611612 <Pallet<T>>::deposit_event(Event::<T>::CollectionAdminRemoved(613 collection_id,614 account_id.clone()615 ));616617 <PalletCommon<T>>::toggle_admin(&collection, &sender, &account_id, false)618 }619620 /// # Permissions621 ///622 /// * Collection Owner623 ///624 /// # Arguments625 ///626 /// * collection_id.627 ///628 /// * new_sponsor.629 #[weight = <SelfWeightOf<T>>::set_collection_sponsor()]630 #[transactional]631 pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {632 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);633634 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;635 target_collection.check_is_owner(&sender)?;636637 target_collection.sponsorship = SponsorshipState::Unconfirmed(new_sponsor.clone());638639 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(640 collection_id,641 new_sponsor642 ));643644 target_collection.save()645 }646647 /// # Permissions648 ///649 /// * Sponsor.650 ///651 /// # Arguments652 ///653 /// * collection_id.654 #[weight = <SelfWeightOf<T>>::confirm_sponsorship()]655 #[transactional]656 pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {657 let sender = ensure_signed(origin)?;658659 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;660 ensure!(661 target_collection.sponsorship.pending_sponsor() == Some(&sender),662 Error::<T>::ConfirmUnsetSponsorFail663 );664665 target_collection.sponsorship = SponsorshipState::Confirmed(sender.clone());666667 <Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(668 collection_id,669 sender670 ));671672 target_collection.save()673 }674675 /// Switch back to pay-per-own-transaction model.676 ///677 /// # Permissions678 ///679 /// * Collection owner.680 ///681 /// # Arguments682 ///683 /// * collection_id.684 #[weight = <SelfWeightOf<T>>::remove_collection_sponsor()]685 #[transactional]686 pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {687 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);688689 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;690 target_collection.check_is_owner(&sender)?;691692 target_collection.sponsorship = SponsorshipState::Disabled;693694 <Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(695 collection_id696 ));697 target_collection.save()698 }699700 /// This method creates a concrete instance of NFT Collection created with CreateCollection method.701 ///702 /// # Permissions703 ///704 /// * Collection Owner.705 /// * Collection Admin.706 /// * Anyone if707 /// * Allow List is enabled, and708 /// * Address is added to allow list, and709 /// * MintPermission is enabled (see SetMintPermission method)710 ///711 /// # Arguments712 ///713 /// * collection_id: ID of the collection.714 ///715 /// * owner: Address, initial owner of the NFT.716 ///717 /// * data: Token data to store on chain.718 #[weight = <CommonWeights<T>>::create_item()]719 #[transactional]720 pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResultWithPostInfo {721 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);722723 dispatch_call::<T, _>(collection_id, |d| d.create_item(sender, owner, data))724 }725726 /// This method creates multiple items in a collection created with CreateCollection method.727 ///728 /// # Permissions729 ///730 /// * Collection Owner.731 /// * Collection Admin.732 /// * Anyone if733 /// * Allow List is enabled, and734 /// * Address is added to allow list, and735 /// * MintPermission is enabled (see SetMintPermission method)736 ///737 /// # Arguments738 ///739 /// * collection_id: ID of the collection.740 ///741 /// * itemsData: Array items properties. Each property is an array of bytes itself, see [create_item].742 ///743 /// * owner: Address, initial owner of the NFT.744 #[weight = <CommonWeights<T>>::create_multiple_items(items_data.len() as u32)]745 #[transactional]746 pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResultWithPostInfo {747 ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);748 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);749750 dispatch_call::<T, _>(collection_id, |d| d.create_multiple_items(sender, owner, items_data))751 }752753 #[weight = <CommonWeights<T>>::create_multiple_items_ex(&data)]754 #[transactional]755 pub fn create_multiple_items_ex(origin, collection_id: CollectionId, data: CreateItemExData<T::CrossAccountId>) -> DispatchResultWithPostInfo {756 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);757758 dispatch_call::<T, _>(collection_id, |d| d.create_multiple_items_ex(sender, data))759 }760761 // TODO! transaction weight762763 /// Set transfers_enabled value for particular collection764 ///765 /// # Permissions766 ///767 /// * Collection Owner.768 ///769 /// # Arguments770 ///771 /// * collection_id: ID of the collection.772 ///773 /// * value: New flag value.774 #[weight = <SelfWeightOf<T>>::set_transfers_enabled_flag()]775 #[transactional]776 pub fn set_transfers_enabled_flag(origin, collection_id: CollectionId, value: bool) -> DispatchResult {777 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);778 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;779 target_collection.check_is_owner(&sender)?;780781 // =========782783 target_collection.limits.transfers_enabled = Some(value);784 target_collection.save()785 }786787 /// Destroys a concrete instance of NFT.788 ///789 /// # Permissions790 ///791 /// * Collection Owner.792 /// * Collection Admin.793 /// * Current NFT Owner.794 ///795 /// # Arguments796 ///797 /// * collection_id: ID of the collection.798 ///799 /// * item_id: ID of NFT to burn.800 #[weight = <CommonWeights<T>>::burn_item()]801 #[transactional]802 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {803 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);804805 let post_info = dispatch_call::<T, _>(collection_id, |d| d.burn_item(sender, item_id, value))?;806 if value == 1 {807 <NftTransferBasket<T>>::remove(collection_id, item_id);808 <NftApproveBasket<T>>::remove(collection_id, item_id);809 }810 // Those maps should be cleared only if token disappears completly, need to move this part of logic to pallets?811 // <FungibleApproveBasket<T>>::remove(collection_id, sender.as_sub());812 // <RefungibleApproveBasket<T>>::remove((collection_id, item_id, sender.as_sub()));813 Ok(post_info)814 }815816 /// Destroys a concrete instance of NFT on behalf of the owner817 /// See also: [`approve`]818 ///819 /// # Permissions820 ///821 /// * Collection Owner.822 /// * Collection Admin.823 /// * Current NFT Owner.824 ///825 /// # Arguments826 ///827 /// * collection_id: ID of the collection.828 ///829 /// * item_id: ID of NFT to burn.830 ///831 /// * from: owner of item832 #[weight = <CommonWeights<T>>::burn_from()]833 #[transactional]834 pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {835 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);836837 dispatch_call::<T, _>(collection_id, |d| d.burn_from(sender, from, item_id, value))838 }839840 /// Change ownership of the token.841 ///842 /// # Permissions843 ///844 /// * Collection Owner845 /// * Collection Admin846 /// * Current NFT owner847 ///848 /// # Arguments849 ///850 /// * recipient: Address of token recipient.851 ///852 /// * collection_id.853 ///854 /// * item_id: ID of the item855 /// * Non-Fungible Mode: Required.856 /// * Fungible Mode: Ignored.857 /// * Re-Fungible Mode: Required.858 ///859 /// * value: Amount to transfer.860 /// * Non-Fungible Mode: Ignored861 /// * Fungible Mode: Must specify transferred amount862 /// * Re-Fungible Mode: Must specify transferred portion (between 0 and 1)863 #[weight = <CommonWeights<T>>::transfer()]864 #[transactional]865 pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {866 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);867868 dispatch_call::<T, _>(collection_id, |d| d.transfer(sender, recipient, item_id, value))869 }870871 /// Set, change, or remove approved address to transfer the ownership of the NFT.872 ///873 /// # Permissions874 ///875 /// * Collection Owner876 /// * Collection Admin877 /// * Current NFT owner878 ///879 /// # Arguments880 ///881 /// * approved: Address that is approved to transfer this NFT or zero (if needed to remove approval).882 ///883 /// * collection_id.884 ///885 /// * item_id: ID of the item.886 #[weight = <CommonWeights<T>>::approve()]887 #[transactional]888 pub fn approve(origin, spender: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResultWithPostInfo {889 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);890891 dispatch_call::<T, _>(collection_id, |d| d.approve(sender, spender, item_id, amount))892 }893894 /// 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.895 ///896 /// # Permissions897 /// * Collection Owner898 /// * Collection Admin899 /// * Current NFT owner900 /// * Address approved by current NFT owner901 ///902 /// # Arguments903 ///904 /// * from: Address that owns token.905 ///906 /// * recipient: Address of token recipient.907 ///908 /// * collection_id.909 ///910 /// * item_id: ID of the item.911 ///912 /// * value: Amount to transfer.913 #[weight = <CommonWeights<T>>::transfer_from()]914 #[transactional]915 pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo {916 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);917918 dispatch_call::<T, _>(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value))919 }920921 /// Set off-chain data schema.922 ///923 /// # Permissions924 ///925 /// * Collection Owner926 /// * Collection Admin927 ///928 /// # Arguments929 ///930 /// * collection_id.931 ///932 /// * schema: String representing the offchain data schema.933 #[weight = <CommonWeights<T>>::set_variable_metadata(data.len() as u32)]934 #[transactional]935 pub fn set_variable_meta_data (936 origin,937 collection_id: CollectionId,938 item_id: TokenId,939 data: BoundedVec<u8, CustomDataLimit>,940 ) -> DispatchResultWithPostInfo {941 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);942943 dispatch_call::<T, _>(collection_id, |d| d.set_variable_metadata(sender, item_id, data))944 }945946 /// Set meta_update_permission value for particular collection947 ///948 /// # Permissions949 ///950 /// * Collection Owner.951 ///952 /// # Arguments953 ///954 /// * collection_id: ID of the collection.955 ///956 /// * value: New flag value.957 #[weight = <SelfWeightOf<T>>::set_meta_update_permission_flag()]958 #[transactional]959 pub fn set_meta_update_permission_flag(origin, collection_id: CollectionId, value: MetaUpdatePermission) -> DispatchResult {960 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);961 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;962963 ensure!(964 target_collection.meta_update_permission != MetaUpdatePermission::None,965 <CommonError<T>>::MetadataFlagFrozen,966 );967 target_collection.check_is_owner(&sender)?;968969 target_collection.meta_update_permission = value;970971 target_collection.save()972 }973974 /// Set schema standard975 /// ImageURL976 /// Unique977 ///978 /// # Permissions979 ///980 /// * Collection Owner981 /// * Collection Admin982 ///983 /// # Arguments984 ///985 /// * collection_id.986 ///987 /// * schema: SchemaVersion: enum988 #[weight = <SelfWeightOf<T>>::set_schema_version()]989 #[transactional]990 pub fn set_schema_version(991 origin,992 collection_id: CollectionId,993 version: SchemaVersion994 ) -> DispatchResult {995 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);996 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;997 target_collection.check_is_owner_or_admin(&sender)?;998 target_collection.schema_version = version;9991000 <Pallet<T>>::deposit_event(Event::<T>::SchemaVersionSet(1001 collection_id1002 ));10031004 target_collection.save()1005 }10061007 /// Set off-chain data schema.1008 ///1009 /// # Permissions1010 ///1011 /// * Collection Owner1012 /// * Collection Admin1013 ///1014 /// # Arguments1015 ///1016 /// * collection_id.1017 ///1018 /// * schema: String representing the offchain data schema.1019 #[weight = <SelfWeightOf<T>>::set_offchain_schema(schema.len() as u32)]1020 #[transactional]1021 pub fn set_offchain_schema(1022 origin,1023 collection_id: CollectionId,1024 schema: BoundedVec<u8, ConstU32<OFFCHAIN_SCHEMA_LIMIT>>,1025 ) -> DispatchResult {1026 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1027 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1028 target_collection.check_is_owner_or_admin(&sender)?;10291030 target_collection.offchain_schema = schema;10311032 <Pallet<T>>::deposit_event(Event::<T>::OffchainSchemaSet(1033 collection_id1034 ));10351036 target_collection.save()1037 }10381039 /// Set const on-chain data schema.1040 ///1041 /// # Permissions1042 ///1043 /// * Collection Owner1044 /// * Collection Admin1045 ///1046 /// # Arguments1047 ///1048 /// * collection_id.1049 ///1050 /// * schema: String representing the const on-chain data schema.1051 #[weight = <SelfWeightOf<T>>::set_const_on_chain_schema(schema.len() as u32)]1052 #[transactional]1053 pub fn set_const_on_chain_schema (1054 origin,1055 collection_id: CollectionId,1056 schema: BoundedVec<u8, ConstU32<CONST_ON_CHAIN_SCHEMA_LIMIT>>1057 ) -> DispatchResult {1058 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1059 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1060 target_collection.check_is_owner_or_admin(&sender)?;10611062 target_collection.const_on_chain_schema = schema;10631064 <Pallet<T>>::deposit_event(Event::<T>::ConstOnChainSchemaSet(1065 collection_id1066 ));10671068 target_collection.save()1069 }10701071 /// Set variable on-chain data schema.1072 ///1073 /// # Permissions1074 ///1075 /// * Collection Owner1076 /// * Collection Admin1077 ///1078 /// # Arguments1079 ///1080 /// * collection_id.1081 ///1082 /// * schema: String representing the variable on-chain data schema.1083 #[weight = <SelfWeightOf<T>>::set_const_on_chain_schema(schema.len() as u32)]1084 #[transactional]1085 pub fn set_variable_on_chain_schema (1086 origin,1087 collection_id: CollectionId,1088 schema: BoundedVec<u8, ConstU32<VARIABLE_ON_CHAIN_SCHEMA_LIMIT>>1089 ) -> DispatchResult {1090 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1091 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1092 target_collection.check_is_owner_or_admin(&sender)?;10931094 target_collection.variable_on_chain_schema = schema;10951096 <Pallet<T>>::deposit_event(Event::<T>::VariableOnChainSchemaSet(1097 collection_id1098 ));10991100 target_collection.save()1101 }11021103 #[weight = <SelfWeightOf<T>>::set_collection_limits()]1104 #[transactional]1105 pub fn set_collection_limits(1106 origin,1107 collection_id: CollectionId,1108 new_limit: CollectionLimits,1109 ) -> DispatchResult {1110 let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1111 let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1112 target_collection.check_is_owner(&sender)?;1113 let old_limit = &target_collection.limits;11141115 target_collection.limits = <PalletCommon<T>>::clamp_limits(target_collection.mode.clone(), &old_limit, new_limit)?;11161117 <Pallet<T>>::deposit_event(Event::<T>::CollectionLimitSet(1118 collection_id1119 ));11201121 target_collection.save()1122 }1123 }1124}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);