git.delta.rocks / unique-network / refs/commits / 2a441390261c

difftreelog

Scheduler refactored and fixed

str-mv2021-06-15parent: #eb5b535.patch.diff
in: master

8 files changed

modifiedpallets/nft-charge-transaction/src/lib.rsdiffbeforeafterboth
--- a/pallets/nft-charge-transaction/src/lib.rs
+++ b/pallets/nft-charge-transaction/src/lib.rs
@@ -135,6 +135,13 @@
 			.map(|i| (fee, i));
         }
 
+        // check errors
+        let error = <pallet_nft_transaction_payment::Module<T>>::check_error(who, call);
+        match error {
+            Err(error) => return Err(error),
+            Ok(error) => {}
+        };
+
         // Determine who is paying transaction fee based on ecnomic model
 		// Parse call to extract collection ID and access collection sponsor	
 		let sponsor = <pallet_nft_transaction_payment::Module<T>>::withdraw_type(who, call);
addedpallets/nft-transaction-payment/README.mddiffbeforeafterboth
--- /dev/null
+++ b/pallets/nft-transaction-payment/README.md
@@ -0,0 +1,13 @@
+# Nft Transaction Payment
+
+## Overview
+
+A module containing the sponsoring logic for paying for sponsored collections
+
+**NOTE:** The scheduled calls will be dispatched with the default filter
+for the origin: namely `frame_system::Config::BaseCallFilter` for all origin
+except root which will get no filter. And not the filter contained in origin
+use to call `fn schedule`.
+
+If a call is scheduled using proxy or whatever mecanism which adds filter,
+then those filter will not be used when dispatching the schedule call.
modifiedpallets/nft-transaction-payment/src/lib.rsdiffbeforeafterboth
before · pallets/nft-transaction-payment/src/lib.rs
1//2// This file is subject to the terms and conditions defined in3// file 'LICENSE', which is part of this source code package.4//56#![cfg_attr(not(feature = "std"), no_std)]78#[cfg(feature = "std")]9pub use std::*;1011#[cfg(feature = "std")]12pub use serde::*;1314#[cfg(feature = "runtime-benchmarks")]15mod benchmarking;1617#[cfg(test)]18mod tests;1920use frame_support::{21	decl_error, decl_module, decl_storage,22	traits::{23		IsSubType, 24	},25	weights::{26		DispatchInfo27	}28};29use sp_runtime::traits::StaticLookup;30use sp_runtime::{31	traits::{ 32		Hash, Dispatchable,33	},34};35use pallet_contracts::chain_extension::UncheckedFrom;36use sp_std::prelude::*;37use nft_data_structs::{38	CreateItemData,39	CollectionId, CollectionMode, TokenId40};4142type CodeHash<T> = <T as frame_system::Config>::Hash;4344	pub trait Config: frame_system::Config + pallet_contracts::Config + pallet_transaction_payment::Config + pallet_nft::Config {45	}4647	// Error for non-fungible-token module.48	49	decl_error! {50		/// Error for non-fungible-token module.51		pub enum Error for Module<T: Config> {52		/// No available class ID53		NoAvailableClassId,54		/// No available token ID55		NoAvailableTokenId,56		/// Token(ClassId, TokenId) not found57		TokenNotFound,58		/// Class not found59		CollectionNotFound,60		/// The operator is not the owner of the token and has no permission61		NoPermission,62		/// Arithmetic calculation overflow63		NumOverflow,64		/// Can not destroy class65		/// Total issuance is not 066		CannotDestroyClass,67	}68}6970	decl_storage! {71		trait Store for Module<T: Config> as NftTransactionPayment{7273	}74}7576decl_module! {7778	pub struct Module<T: Config> for enum Call 79    where 80		origin: T::Origin,81    {82	}83}8485impl<T: Config> Module<T> 86{8788	pub fn withdraw_type(89		who: &T::AccountId,90		call: &T::Call91	) -> Option<T::AccountId> where 92		T::Call: Dispatchable<Info=DispatchInfo>,93		T::Call: IsSubType<pallet_nft::Call<T>>, 94		T::Call: IsSubType<pallet_contracts::Call<T>>,95		T::AccountId: AsRef<[u8]>,96		T::AccountId: UncheckedFrom<T::Hash>97		{9899        let mut sponsor: Option<T::AccountId> = match IsSubType::<pallet_nft::Call<T>>::is_sub_type(call)  {100            Some(pallet_nft::Call::create_item(collection_id, _owner, _properties)) => {101102                Self::withdraw_create_item(who, collection_id, &_properties)103            },104            Some(pallet_nft::Call::transfer(_new_owner, collection_id, item_id, _value)) => {105106                Self::withdraw_transfer(who, collection_id, item_id)107            },108            Some(pallet_nft::Call::set_variable_meta_data(collection_id, item_id, data)) => {109110                Self::withdraw_set_variable_meta_data(who, collection_id, item_id, &data)111			},112			_ => None,113        };114115        sponsor = sponsor.or_else(|| match IsSubType::<pallet_contracts::Call<T>>::is_sub_type(call) {116            Some(pallet_contracts::Call::call(dest, _value, _gas_limit, _data)) => {117118                Self::withdraw_contract_call(who, dest)119            },120            Some(pallet_contracts::Call::instantiate(_endowment, _gas_limit, code_hash, _data, salt)) => {121122                Self::withdraw_contract_instantiate(&who, code_hash, salt)123            },124            Some(pallet_contracts::Call::instantiate_with_code(_endowment, _gas_limit, _code, _data, _salt))  => {125126                Self::withdraw_contract_instantiate(&who, &T::Hashing::hash(&_code), _salt)127			},128			_ => None,129        });130131		sponsor132	}133134135136	pub fn withdraw_create_item(137		who: &T::AccountId,138		collection_id: &CollectionId,139		_properties: &CreateItemData,140	) -> Option<T::AccountId> {141	142		let collection = pallet_nft::CollectionById::<T>::get(collection_id)?;143144		// sponsor timeout145		let block_number = <frame_system::Module<T>>::block_number() as T::BlockNumber;146147		let limit = collection.limits.sponsor_transfer_timeout;148		if pallet_nft::CreateItemBasket::<T>::contains_key((collection_id, &who)) {149			let last_tx_block = pallet_nft::CreateItemBasket::<T>::get((collection_id, &who));150			let limit_time = last_tx_block + limit.into();151			if block_number <= limit_time {152				return None;153			}154		}155		pallet_nft::CreateItemBasket::<T>::insert((collection_id, who.clone()), block_number);156157		// check free create limit158		if collection.limits.sponsored_data_size >= (_properties.len() as u32) {159			collection.sponsorship.sponsor()160				.cloned()161		} else {162			None163		}164	}165166	pub fn withdraw_transfer(167		who: &T::AccountId,168		collection_id: &CollectionId,169		item_id: &TokenId,170	) -> Option<T::AccountId> {171172		let collection = pallet_nft::CollectionById::<T>::get(collection_id)?;173		let limits = pallet_nft::ChainLimit::get();174175		let mut sponsor_transfer = false;176		if collection.sponsorship.confirmed() {177178			let collection_limits = collection.limits.clone();179			let collection_mode = collection.mode.clone();180181			// sponsor timeout182			let block_number = <frame_system::Module<T>>::block_number() as T::BlockNumber;183			sponsor_transfer = match collection_mode {184				CollectionMode::NFT => {185186					// get correct limit187					let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 {188						collection_limits.sponsor_transfer_timeout189					} else {190						limits.nft_sponsor_transfer_timeout191					};192193					let mut sponsored = true;194					if pallet_nft::NftTransferBasket::<T>::contains_key(collection_id, item_id) {195						let last_tx_block = pallet_nft::NftTransferBasket::<T>::get(collection_id, item_id);196						let limit_time = last_tx_block + limit.into();197						if block_number <= limit_time {198							sponsored = false;199						}200					}201					if sponsored {202						pallet_nft::NftTransferBasket::<T>::insert(collection_id, item_id, block_number);203					}204205					sponsored206				}207				CollectionMode::Fungible(_) => {208209					// get correct limit210					let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 {211						collection_limits.sponsor_transfer_timeout212					} else {213						limits.fungible_sponsor_transfer_timeout214					};215216					let block_number = <frame_system::Module<T>>::block_number() as T::BlockNumber;217					let mut sponsored = true;218					if pallet_nft::FungibleTransferBasket::<T>::contains_key(collection_id, who) {219						let last_tx_block = pallet_nft::FungibleTransferBasket::<T>::get(collection_id, who);220						let limit_time = last_tx_block + limit.into();221						if block_number <= limit_time {222							sponsored = false;223						}224					}225					if sponsored {226						pallet_nft::FungibleTransferBasket::<T>::insert(collection_id, who, block_number);227					}228229					sponsored230				}231				CollectionMode::ReFungible => {232233					// get correct limit234					let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 {235						collection_limits.sponsor_transfer_timeout236					} else {237						limits.refungible_sponsor_transfer_timeout238					};239240					let mut sponsored = true;241					if pallet_nft::ReFungibleTransferBasket::<T>::contains_key(collection_id, item_id) {242						let last_tx_block = pallet_nft::ReFungibleTransferBasket::<T>::get(collection_id, item_id);243						let limit_time = last_tx_block + limit.into();244						if block_number <= limit_time {245							sponsored = false;246						}247					}248					if sponsored {249						pallet_nft::ReFungibleTransferBasket::<T>::insert(collection_id, item_id, block_number);250					}251252					sponsored253				}254				_ => {255					false256				},257			};258		}259260		if !sponsor_transfer {261			None262		} else {263			collection.sponsorship.sponsor()264				.cloned()265		}266	}267	268	pub fn withdraw_set_variable_meta_data(269		who: &T::AccountId,270		collection_id: &CollectionId,271		item_id: &TokenId,272		data: &Vec<u8>,273	) -> Option<T::AccountId> {274275		let mut sponsor_metadata_changes = false;276277		let collection = pallet_nft::CollectionById::<T>::get(collection_id)?;278279		if280			collection.sponsorship.confirmed() &&281			// Can't sponsor fungible collection, this tx will be rejected282			// as invalid283			!matches!(collection.mode, CollectionMode::Fungible(_)) &&284			data.len() <= collection.limits.sponsored_data_size as usize285		{286			if let Some(rate_limit) = collection.limits.sponsored_data_rate_limit {287				let block_number = <frame_system::Module<T>>::block_number() as T::BlockNumber;288289				if pallet_nft::VariableMetaDataBasket::<T>::get(collection_id, item_id)290					.map(|last_block| block_number - last_block > rate_limit)291					.unwrap_or(true) 292				{293					sponsor_metadata_changes = true;294					pallet_nft::VariableMetaDataBasket::<T>::insert(collection_id, item_id, block_number);295				}296			}297		}298299		if !sponsor_metadata_changes {300			None301		} else {302			collection.sponsorship.sponsor().cloned()303		}304305	}306307	pub fn withdraw_contract_call(308		who: &T::AccountId,309		dest: &<T::Lookup as StaticLookup>::Source310	) -> Option<T::AccountId> {311312		let called_contract: T::AccountId = T::Lookup::lookup((dest).clone()).unwrap_or(T::AccountId::default());313314		let owned_contract = pallet_nft::ContractOwner::<T>::get(called_contract.clone()).as_ref() == Some(who);315		let white_list_enabled = pallet_nft::ContractWhiteListEnabled::<T>::contains_key(called_contract.clone());316		  317		// ???318		if !owned_contract && white_list_enabled {319		 	if !pallet_nft::ContractWhiteList::<T>::contains_key(called_contract.clone(), who) {320				return Some(who.clone())321		 		// return Err(InvalidTransaction::Call.into());322		 	}323		}324325		let mut sponsor_transfer = false;326		if pallet_nft::ContractSponsoringRateLimit::<T>::contains_key(called_contract.clone()) {327			let last_tx_block = pallet_nft::ContractSponsorBasket::<T>::get((&called_contract, &who));328			let block_number = <frame_system::Module<T>>::block_number() as T::BlockNumber;329			let rate_limit = pallet_nft::ContractSponsoringRateLimit::<T>::get(&called_contract);330			let limit_time = last_tx_block + rate_limit;331332			if block_number >= limit_time {333				pallet_nft::ContractSponsorBasket::<T>::insert((called_contract.clone(), who.clone()), block_number);334				sponsor_transfer = true;335			}336		} else {337			sponsor_transfer = false;338		}339	   340		if sponsor_transfer {341			if pallet_nft::ContractSelfSponsoring::<T>::contains_key(called_contract.clone()) {342				if pallet_nft::ContractSelfSponsoring::<T>::get(called_contract.clone()) {343					return Some(called_contract);344				}345			}346		}347348		None349	}350351	pub fn withdraw_contract_instantiate(352		who: &T::AccountId,353		code_hash: &CodeHash<T>,354		salt: &[u8],355	) -> Option<T::AccountId> where356	T::AccountId: AsRef<[u8]>,357	T::AccountId: UncheckedFrom<T::Hash>358	{359360		let new_contract_address = <pallet_contracts::Module<T>>::contract_address(361			&who,362			code_hash,363			salt,364		);365		pallet_nft::ContractOwner::<T>::insert(new_contract_address.clone(), who.clone());366367		None368	}369}370371// type BalanceOf<T> = <<T as pallet_transaction_payment::Config>::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction<T>>::Balance;372373// /// Require the transactor pay for themselves and maybe include a tip to gain additional priority374// /// in the queue.375// #[derive(Encode, Decode, Clone, Eq, PartialEq)]376// pub struct ChargeTransactionPayment<T: Config>(#[codec(compact)] BalanceOf<T>);377378// impl<T: Config + Send + Sync> sp_std::fmt::Debug 379//     for ChargeTransactionPayment<T>380// {381// 	#[cfg(feature = "std")]382// 	fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {383// 		write!(f, "ChargeTransactionPayment<{:?}>", self.0)384// 	}385// 	#[cfg(not(feature = "std"))]386// 	fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {387// 		Ok(())388// 	}389// }390391// impl<T: Config> ChargeTransactionPayment<T>392// where393// 	T::Call: Dispatchable<Info=DispatchInfo, PostInfo=PostDispatchInfo> + IsSubType<pallet_nft::Call<T>> + IsSubType<pallet_contracts::Call<T>>,394//     BalanceOf<T>: Send + Sync + From<u64> + FixedPointOperand,395//     T::AccountId: AsRef<[u8]>,396//     T::AccountId: UncheckedFrom<T::Hash>,397// {398//     fn traditional_fee(399//         len: usize,400//         info: &DispatchInfoOf<T::Call>,401//         tip: BalanceOf<T>,402//     ) -> BalanceOf<T>403//     where404//         T::Call: Dispatchable<Info = DispatchInfo>,405//     {406//         <pallet_transaction_payment::Module<T>>::compute_fee(len as u32, info, tip)407//     }408409// 	fn get_priority(len: usize, info: &DispatchInfoOf<T::Call>, final_fee: BalanceOf<T>) -> TransactionPriority {410//         let weight_saturation = T::BlockWeights::get().max_block / info.weight.max(1);411//         let max_block_length = *T::BlockLength::get().max.get(DispatchClass::Normal);412//         let len_saturation = max_block_length as u64 / (len as u64).max(1);413//         let coefficient: BalanceOf<T> = weight_saturation414//             .min(len_saturation)415//             .saturated_into::<BalanceOf<T>>();416//         final_fee417//             .saturating_mul(coefficient)418//             .saturated_into::<TransactionPriority>()419//     }420421//     fn withdraw_fee(422//         &self,423//         who: &T::AccountId,424//         call: &T::Call,425//         info: &DispatchInfoOf<T::Call>,426//         len: usize,427// 	) -> Result<428// 		(429// 			BalanceOf<T>,430// 			<<T as pallet_transaction_payment::Config>::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction<T>>::LiquidityInfo,431// 		),432// 		TransactionValidityError,433// 	> {434//         let tip = self.0;435436//         let fee = Self::traditional_fee(len, info, tip);437438//         // Only mess with balances if fee is not zero.439//         if fee.is_zero() {440//             return <<T as pallet_transaction_payment::Config>::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction<T>>::withdraw_fee(who, call, info, fee, tip)441// 			.map(|i| (fee, i));442//         }443444//         // Determine who is paying transaction fee based on ecnomic model445// 		// Parse call to extract collection ID and access collection sponsor446		447// 		let sponsor = pallet_nft_transaction_payment::<T>::withdraw_type(who, call);448// 		//let sponsor = Self::Module::<T>::withdraw_type(who, call);;449// 		// <pallet_nft_transaction_payment::Module<T>>::withdraw_type(who, call);450451//         let who_pays_fee = sponsor.unwrap_or_else(|| who.clone());452453// 		<<T as pallet_transaction_payment::Config>::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction<T>>::withdraw_fee(&who_pays_fee, call, info, fee, tip)454// 			.map(|i| (fee, i))455//     }456// }457458459// impl<T: Config + Send + Sync> SignedExtension460//     for ChargeTransactionPayment<T>461// where462//     BalanceOf<T>: Send + Sync + From<u64> + FixedPointOperand,463// 	T::Call: Dispatchable<Info=DispatchInfo, PostInfo=PostDispatchInfo> + IsSubType<pallet_nft::Call<T>> + IsSubType<pallet_contracts::Call<T>>,464//     T::AccountId: AsRef<[u8]>,465//     T::AccountId: UncheckedFrom<T::Hash>,466// {467//     const IDENTIFIER: &'static str = "ChargeTransactionPayment";468//     type AccountId = T::AccountId;469//     type Call = T::Call;470//     type AdditionalSigned = ();471//     type Pre = (472//         // tip473//         BalanceOf<T>,474//         // who pays fee475//         Self::AccountId,476// 		// imbalance resulting from withdrawing the fee477// 		<<T as pallet_transaction_payment::Config>::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction<T>>::LiquidityInfo,478//     );479//     fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> {480//         Ok(())481//     }482483//     fn validate(484//         &self,485//         who: &Self::AccountId,486//         call: &Self::Call,487//         info: &DispatchInfoOf<Self::Call>,488//         len: usize,489//     ) -> TransactionValidity {490// 		let (fee, _) = self.withdraw_fee(who, call, info, len)?;491// 		Ok(ValidTransaction {492// 			priority: Self::get_priority(len, info, fee),493// 			..Default::default()494// 		})495//     }496497//     fn pre_dispatch(498//         self,499//         who: &Self::AccountId,500//         call: &Self::Call,501//         info: &DispatchInfoOf<Self::Call>,502//         len: usize,503//     ) -> Result<Self::Pre, TransactionValidityError> {504//         let (_fee, imbalance) = self.withdraw_fee(who, call, info, len)?;505//         Ok((self.0, who.clone(), imbalance))506//     }507508//     fn post_dispatch(509//         pre: Self::Pre,510//         info: &DispatchInfoOf<Self::Call>,511//         post_info: &PostDispatchInfoOf<Self::Call>,512//         len: usize,513//         _result: &DispatchResult,514//     ) -> Result<(), TransactionValidityError> {515// 		let (tip, who, imbalance) = pre;516// 		let actual_fee = pallet_transaction_payment::Module::<T>::compute_actual_fee(517// 			len as u32,518// 			info,519// 			post_info,520// 			tip,521// 		);522// 		<T as pallet_transaction_payment::Config>::OnChargeTransaction::correct_and_deposit_fee(&who, info, post_info, actual_fee, tip, imbalance)?;523// 		Ok(())524//     }525// }
modifiedpallets/nft/src/lib.rsdiffbeforeafterboth
--- a/pallets/nft/src/lib.rs
+++ b/pallets/nft/src/lib.rs
@@ -32,10 +32,11 @@
 
 use frame_system::{self as system, ensure_signed, ensure_root};
 use sp_runtime::sp_std::prelude::Vec;
+use core::ops::{Deref, DerefMut};
 use nft_data_structs::{
     MAX_DECIMAL_POINTS, MAX_SPONSOR_TIMEOUT, MAX_TOKEN_OWNERSHIP, MAX_REFUNGIBLE_PIECES,
 	AccessMode, ChainLimits, Collection, CreateItemData, CollectionLimits,
-    CollectionId, CollectionMode, CollectionHandle, TokenId, 
+    CollectionId, CollectionMode, TokenId, 
     SchemaVersion, SponsorshipState, Ownership,
     NftItemType, FungibleItemType, ReFungibleItemType
 };
@@ -164,7 +165,25 @@
 	}
 }
 
-// + pallet_transaction_payment::Config  + pallet_nft_transaction_payment::Config + pallet_contracts::Config
+pub struct CollectionHandle<T: frame_system::Config> {
+    pub id: CollectionId,
+    pub collection: Collection<T>,
+}
+
+impl<T: frame_system::Config> Deref for CollectionHandle<T> {
+    type Target = Collection<T>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.collection
+    }
+}
+
+impl<T: frame_system::Config> DerefMut for CollectionHandle<T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.collection
+    }
+}
+
 
 
 pub trait Config: system::Config + Sized {
modifiedpallets/scheduler/src/lib.rsdiffbeforeafterboth
--- a/pallets/scheduler/src/lib.rs
+++ b/pallets/scheduler/src/lib.rs
@@ -102,7 +102,7 @@
 	/// Not strictly enforced, but used for weight estimation.
 	type MaxScheduledPerBlock: Get<u32>;
 
-	/// TODO
+	/// Sponsoring function
 	type Sponsoring: SponsoringResolve<Self::AccountId, <Self as Config>::Call>;
 
 	/// Weight information for extrinsics in this pallet.
@@ -230,8 +230,7 @@
 		///     - Write: Agenda
 		/// - Will use base weight of 25 which should be good for up to 30 scheduled calls
 		/// # </weight>
-		// #[weight = <T as Config>::WeightInfo::schedule(T::MaxScheduledPerBlock::get())]
-		#[weight = 0]
+		#[weight = <T as Config>::WeightInfo::schedule(T::MaxScheduledPerBlock::get())]
 		fn schedule(origin,
 			when: T::BlockNumber,
 			maybe_periodic: Option<schedule::Period<T::BlockNumber>>,
@@ -367,7 +366,7 @@
 			}
 			queued.sort_by_key(|(_, s)| s.priority);
 			let base_weight: Weight = T::DbWeight::get().reads_writes(1, 2); // Agenda + Agenda(next)
-			let mut total_weight: Weight = <T as Config>::WeightInfo::schedule(T::MaxScheduledPerBlock::get());
+			let mut total_weight: Weight = 0;
 			queued.into_iter()
 				.enumerate()
 				.scan(base_weight, |cumulative_weight, (order, (index, s))| {
@@ -407,7 +406,7 @@
 						).into();
 						let sender = ensure_signed(origin).unwrap_or(T::AccountId::default());
 						let who_will_pay = T::Sponsoring::resolve(&sender, &s.call.clone()).unwrap_or(
-							T::AccountId::default());
+							sender);
 						let sponsor = T::PalletsOrigin::from(system::RawOrigin::Signed(who_will_pay));
 						let r = s.call.clone().dispatch(sponsor.into());
 						let maybe_id = s.maybe_id.clone();
modifiedtests/src/pallet-presence.test.tsdiffbeforeafterboth
--- a/tests/src/pallet-presence.test.ts
+++ b/tests/src/pallet-presence.test.ts
@@ -13,7 +13,8 @@
 
 // Pallets that must always be present
 const requiredPallets = [
-  'nft', 'inflation', 'balances', 'contracts', 'randomnesscollectiveflip', 'system', 'timestamp', 'transactionpayment', 'treasury', 'vesting'
+  'nft', 'inflation', 'balances', 'contracts', 'randomnesscollectiveflip', 'system', 'timestamp', 'transactionpayment', 'treasury', 'vesting',
+  'scheduler', 'nftpayment', 'charging'
 ];
 
 // Pallets that depend on consensus and governance configuration
addedtests/src/scheduler.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/scheduler.test.ts
@@ -0,0 +1,94 @@
+//
+// This file is subject to the terms and conditions defined in
+// file 'LICENSE', which is part of this source code package.
+//
+
+import { IKeyringPair } from '@polkadot/types/types';
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import privateKey from './substrate/privateKey';
+import usingApi from './substrate/substrate-api';
+import {
+  createItemExpectSuccess,
+  createCollectionExpectSuccess,
+  destroyCollectionExpectSuccess,
+  findNotExistingCollection,
+  queryCollectionExpectSuccess,
+  setOffchainSchemaExpectFailure,
+  setOffchainSchemaExpectSuccess,
+  transferExpectSuccess,
+  scheduleTransferExpectSuccess,
+  setCollectionSponsorExpectSuccess,
+  confirmSponsorshipExpectSuccess
+} from './util/helpers';
+
+
+chai.use(chaiAsPromised);
+const expect = chai.expect;
+
+const DATA = [1, 2, 3, 4];
+
+describe('Integration Test scheduler base transaction', () => {
+  let alice: IKeyringPair;
+
+  before(async () => {
+    await usingApi(async () => {
+      alice = privateKey('//Alice');
+    });
+  });
+
+  it('User can transfer owned token with delay (scheduler)', async () => {
+    await usingApi(async (api) => {
+      const Alice = privateKey('//Alice');
+      const Bob = privateKey('//Bob');
+      // nft
+      const nftCollectionId = await createCollectionExpectSuccess();
+      const newNftTokenId = await createItemExpectSuccess(Alice, nftCollectionId, 'NFT');
+      await setCollectionSponsorExpectSuccess(nftCollectionId, Alice.address);
+      await confirmSponsorshipExpectSuccess(nftCollectionId);
+
+      await scheduleTransferExpectSuccess(nftCollectionId, newNftTokenId, Alice, Bob, 1, 'NFT');
+    });
+  });
+
+  
+});
+
+// describe('Negative Integration Test setOffchainSchema', () => {
+//   let alice: IKeyringPair;
+//   let bob: IKeyringPair;
+
+//   let validCollectionId: number;
+
+//   before(async () => {
+//     await usingApi(async () => {
+//       alice = privateKey('//Alice');
+//       bob = privateKey('//Bob');
+
+//       validCollectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } });
+//     });
+//   });
+
+//   it('fails on not existing collection id', async () => {
+//     const nonExistingCollectionId = await usingApi(findNotExistingCollection);
+
+//     await setOffchainSchemaExpectFailure(alice, nonExistingCollectionId, DATA);
+//   });
+
+//   it('fails on destroyed collection id', async () => {
+//     const destroyedCollectionId = await createCollectionExpectSuccess({ mode: { type: 'NFT' } });
+//     await destroyCollectionExpectSuccess(destroyedCollectionId);
+
+//     await setOffchainSchemaExpectFailure(alice, destroyedCollectionId, DATA);
+//   });
+
+//   it('fails on too long data', async () => {
+//     const tooLongData = new Array(4097).fill(0xff);
+
+//     await setOffchainSchemaExpectFailure(alice, validCollectionId, tooLongData);
+//   });
+
+//   it('fails on execution by non-owner', async () => {
+//     await setOffchainSchemaExpectFailure(bob, validCollectionId, DATA);
+//   });
+// });
modifiedtests/src/util/helpers.tsdiffbeforeafterboth
--- a/tests/src/util/helpers.ts
+++ b/tests/src/util/helpers.ts
@@ -5,7 +5,7 @@
 
 import { ApiPromise, Keyring } from '@polkadot/api';
 import { Enum, Struct } from '@polkadot/types/codec';
-import type { AccountId, EventRecord } from '@polkadot/types/interfaces';
+import type { AccountId, BlockNumber, Call, EventRecord } from '@polkadot/types/interfaces';
 import { u128 } from '@polkadot/types/primitive';
 import { IKeyringPair } from '@polkadot/types/types';
 import { BigNumber } from 'bignumber.js';
@@ -17,6 +17,8 @@
 import { default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync } from '../substrate/substrate-api';
 import { ICollectionInterface } from '../types';
 import { hexToStr, strToUTF16, utf16ToStr } from './util';
+import { Compact, Option, Raw, Vec } from '@polkadot/types/codec';
+// import { AccountId, AccountIdOf, AccountIndex, Address, AssetId, Balance, BalanceOf, Block, BlockNumber, Call, ChangesTrieConfiguration, Consensus, ConsensusEngineId, Digest, DigestItem, DispatchClass, DispatchInfo, DispatchInfoTo190, EcdsaSignature, Ed25519Signature, Extrinsic, ExtrinsicEra, ExtrinsicPayload, ExtrinsicPayloadUnknown, ExtrinsicPayloadV1, ExtrinsicPayloadV2, ExtrinsicPayloadV3, ExtrinsicPayloadV4, ExtrinsicSignatureV1, ExtrinsicSignatureV2, ExtrinsicSignatureV3, ExtrinsicSignatureV4, ExtrinsicUnknown, ExtrinsicV1, ExtrinsicV2, ExtrinsicV3, ExtrinsicV4, Hash, Header, ImmortalEra, Index, Justification, KeyTypeId, KeyValue, LockIdentifier, LookupSource, LookupTarget, Moment, MortalEra, MultiSignature, Origin, Perbill, Percent, Permill, Perquintill, Phantom, PhantomData, PreRuntime, Seal, SealV0, Signature, SignedBlock, SignerPayload, Sr25519Signature, ValidatorId, Weight, WeightMultiplier } from '@polkadot/types/interfaces/runtime';
 
 chai.use(chaiAsPromised);
 const expect = chai.expect;
@@ -702,6 +704,54 @@
   });
 }
 
+async function getBlockNumber(api: ApiPromise): Promise<number> {
+  return new Promise<number>(async (resolve, reject) => {
+    const unsubscribe = await api.rpc.chain.subscribeNewHeads((head) => {
+        unsubscribe();
+        resolve(head.number.toNumber());
+    });
+  });
+}
+
+export async function
+scheduleTransferExpectSuccess(collectionId: number,
+                      tokenId: number,
+                      sender: IKeyringPair,
+                      recipient: IKeyringPair,
+                      value: number | bigint = 1,
+                      type: string = 'NFT') {
+  await usingApi(async (api: ApiPromise) => {
+    let balanceBefore = new BN(0);
+
+
+    let blockNumber: number | undefined = await getBlockNumber(api);
+    let expectedBlockNumber = blockNumber + 2;
+
+    expect(blockNumber).to.be.greaterThan(0);
+    const transferTx = await api.tx.nft.transfer(recipient.address, collectionId, tokenId, value); 
+    const scheduleTx = await api.tx.scheduler.schedule(expectedBlockNumber, null, 0, transferTx);
+
+    const events = await submitTransactionAsync(sender, scheduleTx);
+
+    const sponsorBalanceBefore = new BigNumber((await api.query.system.account(sender.address)).data.free.toString());
+    const recipientBalanceBefore = new BigNumber((await api.query.system.account(recipient.address)).data.free.toString());
+
+    const nftItemDataBefore = (await api.query.nft.nftItemList(collectionId, tokenId)).toJSON() as unknown as ITokenDataType;
+    expect(nftItemDataBefore.Owner.toString()).to.be.equal(sender.address);
+
+    // sleep for 2 blocks
+    await new Promise(resolve => setTimeout(resolve, 6000 * 2));
+
+    const sponsorBalanceAfter = new BigNumber((await api.query.system.account(sender.address)).data.free.toString());
+    const recipientBalanceAfter = new BigNumber((await api.query.system.account(recipient.address)).data.free.toString());
+
+    const nftItemData = (await api.query.nft.nftItemList(collectionId, tokenId)).toJSON() as unknown as ITokenDataType;
+    expect(nftItemData.Owner.toString()).to.be.equal(recipient.address);
+    expect(recipientBalanceAfter.toNumber()).to.be.equal(recipientBalanceBefore.toNumber());
+  });
+}
+
+
 export async function
 transferExpectSuccess(collectionId: number,
                       tokenId: number,