From eb5b53586bb9f0b6239a83e300f783aeb68d7ddf Mon Sep 17 00:00:00 2001 From: str-mv Date: Thu, 27 May 2021 13:14:58 +0000 Subject: [PATCH] Scheduler --- --- a/Cargo.lock +++ b/Cargo.lock @@ -3556,6 +3556,7 @@ "futures 0.3.12", "jsonrpc-core", "log", + "nft-data-structs", "nft-runtime", "pallet-contracts-rpc", "pallet-transaction-payment-rpc", @@ -3592,6 +3593,19 @@ ] [[package]] +name = "nft-data-structs" +version = "0.9.0" +dependencies = [ + "frame-support", + "frame-system", + "pallet-contracts", + "parity-scale-codec", + "serde", + "sp-core", + "sp-runtime", +] + +[[package]] name = "nft-runtime" version = "3.0.0" dependencies = [ @@ -3602,6 +3616,7 @@ "frame-system-benchmarking", "frame-system-rpc-runtime-api", "hex-literal", + "nft-data-structs", "pallet-aura", "pallet-balances", "pallet-contracts", @@ -3610,7 +3625,10 @@ "pallet-grandpa", "pallet-inflation", "pallet-nft", + "pallet-nft-charge-transaction", + "pallet-nft-transaction-payment", "pallet-randomness-collective-flip", + "pallet-scheduler", "pallet-sudo", "pallet-timestamp", "pallet-transaction-payment", @@ -3947,8 +3965,32 @@ "frame-benchmarking", "frame-support", "frame-system", + "nft-data-structs", + "pallet-balances", + "pallet-contracts", + "pallet-randomness-collective-flip", + "pallet-timestamp", + "pallet-transaction-payment", + "parity-scale-codec", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "pallet-nft-charge-transaction" +version = "3.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "nft-data-structs", "pallet-balances", "pallet-contracts", + "pallet-nft", + "pallet-nft-transaction-payment", "pallet-randomness-collective-flip", "pallet-timestamp", "pallet-transaction-payment", @@ -3961,6 +4003,28 @@ ] [[package]] +name = "pallet-nft-transaction-payment" +version = "3.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "nft-data-structs", + "pallet-balances", + "pallet-contracts", + "pallet-nft", + "pallet-randomness-collective-flip", + "pallet-timestamp", + "pallet-transaction-payment", + "parity-scale-codec", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + +[[package]] name = "pallet-randomness-collective-flip" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3975,6 +4039,27 @@ ] [[package]] +name = "pallet-scheduler" +version = "3.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "nft-data-structs", + "pallet-contracts", + "pallet-nft", + "pallet-nft-transaction-payment", + "parity-scale-codec", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "substrate-test-utils", +] + +[[package]] name = "pallet-session" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -7316,6 +7401,28 @@ ] [[package]] +name = "substrate-test-utils" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f46809281d923956cdfdb2f95e07fa2437884f2a18096c4371a3fa48f8d082a" +dependencies = [ + "futures 0.3.12", + "substrate-test-utils-derive", + "tokio 0.2.25", +] + +[[package]] +name = "substrate-test-utils-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "988016ca49cfc763fecf97dfa9dee7dbbff1a2145b003d07681278dddefd1a0e" +dependencies = [ + "proc-macro-crate", + "quote", + "syn", +] + +[[package]] name = "substrate-wasm-builder" version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -7550,6 +7657,7 @@ "pin-project-lite 0.1.11", "signal-hook-registry", "slab", + "tokio-macros", "winapi 0.3.9", ] @@ -7618,6 +7726,17 @@ ] [[package]] +name = "tokio-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "tokio-named-pipes" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,8 @@ members = [ 'node', 'pallets/nft', + 'pallets/nft-transaction-payment', + 'primitives', 'runtime', ] [profile.release] --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -28,6 +28,7 @@ # Substrate dependencies nft-runtime = { path = '../runtime', version = '3.0.0' } +nft-data-structs = { default-features = false, path="../primitives" } frame-benchmarking = '3.0.0' frame-benchmarking-cli = '3.0.0' pallet-transaction-payment-rpc = '3.0.0' --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -3,19 +3,14 @@ // file 'LICENSE', which is part of this source code package. // -// use nft_runtime::{ -// AccountId, AuraConfig, BalancesConfig, GenesisConfig, GrandpaConfig, Signature, SudoConfig, -// SystemConfig, WASM_BINARY, -// }; -// use nft_runtime::{ContractsConfig, ContractsSchedule, NftConfig, CollectionType}; use nft_runtime::*; +use nft_data_structs::*; use sc_service::ChainType; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{sr25519, Pair, Public}; use sp_finality_grandpa::AuthorityId as GrandpaId; use sp_runtime::traits::{IdentifyAccount, Verify}; use serde_json::map::Map; -// use crate::chain_spec::api::chain_extension::*; // Note this is the URL for the telemetry server //const STAGING_TELEMETRY_URL: &str = "wss://telemetry.polkadot.io/submit/"; @@ -158,7 +153,7 @@ balances: endowed_accounts .iter() .cloned() - .map(|k| (k, 1 << 100)) + .map(|k| (k, 1 << 70)) .collect(), }), pallet_aura: Some(AuraConfig { --- /dev/null +++ b/pallets/nft-charge-transaction/Cargo.toml @@ -0,0 +1,59 @@ +[package] +authors = ['Substrate DevHub '] +description = 'Unqiue pallet nft specific transaction payment' +edition = '2018' +homepage = 'https://substrate.io' +license = 'Unlicense' +name = 'pallet-nft-charge-transaction' +repository = 'https://github.com/usetech-llc/nft_private/' +version = '3.0.0' + +[package.metadata.docs.rs] +targets = ['x86_64-unknown-linux-gnu'] + +# alias "parity-scale-code" to "codec" +[dependencies.codec] +default-features = false +features = ['derive'] +package = 'parity-scale-codec' +version = '2.0.0' + +[dependencies] +serde = { version = "1.0.119" } +frame-support = { default-features = false, version = '3.0.0' } +frame-system = { default-features = false, version = '3.0.0' } +pallet-balances = { default-features = false, version = '3.0.0' } +pallet-contracts = { default-features = false, version = '3.0.0' } +pallet-timestamp = { default-features = false, version = '3.0.0' } +pallet-transaction-payment = { default-features = false, version = "3.0.0" } +pallet-randomness-collective-flip = { default-features = false, version = '3.0.0' } +sp-std = { default-features = false, version = '3.0.0' } +frame-benchmarking = { default-features = false, version = "3.0.0", optional = true } +sp-core = { default-features = false, version = '3.0.0' } +sp-io = { default-features = false, version = '3.0.0' } +sp-runtime = { default-features = false, version = '3.0.0' } + +pallet-nft = { default-features = false, path="../nft" } +pallet-nft-transaction-payment = { default-features = false, path="../nft-transaction-payment" } +nft-data-structs = { default-features = false, path="../../primitives" } + +[features] +default = ['std'] +std = [ + 'codec/std', + 'serde/std', + 'frame-support/std', + 'frame-system/std', + 'pallet-balances/std', + 'pallet-timestamp/std', + 'pallet-randomness-collective-flip/std', + 'pallet-contracts/std', + 'pallet-nft/std', + 'pallet-transaction-payment/std', + 'pallet-nft-transaction-payment/std', + 'sp-std/std', + 'sp-runtime/std', + 'nft-data-structs/std', + 'frame-benchmarking/std', +] +runtime-benchmarks = ["frame-benchmarking"] --- /dev/null +++ b/pallets/nft-charge-transaction/src/lib.rs @@ -0,0 +1,215 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "std")] +pub use std::*; + +#[cfg(feature = "std")] +pub use serde::*; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +#[cfg(test)] +mod tests; + +use codec::{Decode, Encode}; +use frame_support::traits::{ Get}; +use frame_support::{ + decl_module, decl_storage, + traits::{ + IsSubType, + }, + weights::{ + DispatchInfo, PostDispatchInfo, DispatchClass + } +}; +use sp_runtime::{ + traits::{ + DispatchInfoOf, Dispatchable, PostDispatchInfoOf, Saturating, SaturatedConversion, SignedExtension, Zero, + }, + transaction_validity::{ + TransactionPriority, TransactionValidity, TransactionValidityError, ValidTransaction, + }, + FixedPointOperand, DispatchResult +}; +use pallet_contracts::chain_extension::UncheckedFrom; +use pallet_transaction_payment::OnChargeTransaction; +use sp_std::prelude::*; + + + pub trait Config: frame_system::Config + + pallet_nft_transaction_payment::Config + { + + } + + decl_storage! { + trait Store for Module as NftTransactionPayment{ + + } +} + +decl_module! { + + pub struct Module for enum Call + where + origin: T::Origin, + {} +} + +type BalanceOf = <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::Balance; + +/// Require the transactor pay for themselves and maybe include a tip to gain additional priority +/// in the queue. +#[derive(Encode, Decode, Clone, Eq, PartialEq)] +pub struct ChargeTransactionPayment(#[codec(compact)] BalanceOf); + +impl sp_std::fmt::Debug + for ChargeTransactionPayment +{ + #[cfg(feature = "std")] + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!(f, "ChargeTransactionPayment<{:?}>", self.0) + } + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + Ok(()) + } +} + +impl ChargeTransactionPayment +where + T::Call: Dispatchable + IsSubType> + IsSubType>, + BalanceOf: Send + Sync + From + FixedPointOperand, + T::AccountId: AsRef<[u8]>, + T::AccountId: UncheckedFrom, +{ + fn traditional_fee( + len: usize, + info: &DispatchInfoOf, + tip: BalanceOf, + ) -> BalanceOf + where + T::Call: Dispatchable, + { + >::compute_fee(len as u32, info, tip) + } + + fn get_priority(len: usize, info: &DispatchInfoOf, final_fee: BalanceOf) -> TransactionPriority { + let weight_saturation = T::BlockWeights::get().max_block / info.weight.max(1); + let max_block_length = *T::BlockLength::get().max.get(DispatchClass::Normal); + let len_saturation = max_block_length as u64 / (len as u64).max(1); + let coefficient: BalanceOf = weight_saturation + .min(len_saturation) + .saturated_into::>(); + final_fee + .saturating_mul(coefficient) + .saturated_into::() + } + + fn withdraw_fee( + &self, + who: &T::AccountId, + call: &T::Call, + info: &DispatchInfoOf, + len: usize, + ) -> Result< + ( + BalanceOf, + <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::LiquidityInfo, + ), + TransactionValidityError, + > { + let tip = self.0; + + let fee = Self::traditional_fee(len, info, tip); + + // Only mess with balances if fee is not zero. + if fee.is_zero() { + return <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::withdraw_fee(who, call, info, fee, tip) + .map(|i| (fee, i)); + } + + // Determine who is paying transaction fee based on ecnomic model + // Parse call to extract collection ID and access collection sponsor + let sponsor = >::withdraw_type(who, call); + + let who_pays_fee = sponsor.unwrap_or_else(|| who.clone()); + + <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::withdraw_fee(&who_pays_fee, call, info, fee, tip) + .map(|i| (fee, i)) + } +} + +impl SignedExtension + for ChargeTransactionPayment +where + BalanceOf: Send + Sync + From + FixedPointOperand, + T::Call: Dispatchable + IsSubType> + IsSubType>, + T::AccountId: AsRef<[u8]>, + T::AccountId: UncheckedFrom, +{ + const IDENTIFIER: &'static str = "ChargeTransactionPayment"; + type AccountId = T::AccountId; + type Call = T::Call; + type AdditionalSigned = (); + type Pre = ( + // tip + BalanceOf, + // who pays fee + Self::AccountId, + // imbalance resulting from withdrawing the fee + <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::LiquidityInfo, + ); + fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> { + Ok(()) + } + + fn validate( + &self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf, + len: usize, + ) -> TransactionValidity { + let (fee, _) = self.withdraw_fee(who, call, info, len)?; + Ok(ValidTransaction { + priority: Self::get_priority(len, info, fee), + ..Default::default() + }) + } + + fn pre_dispatch( + self, + who: &Self::AccountId, + call: &Self::Call, + info: &DispatchInfoOf, + len: usize, + ) -> Result { + let (_fee, imbalance) = self.withdraw_fee(who, call, info, len)?; + Ok((self.0, who.clone(), imbalance)) + } + + fn post_dispatch( + pre: Self::Pre, + info: &DispatchInfoOf, + post_info: &PostDispatchInfoOf, + len: usize, + _result: &DispatchResult, + ) -> Result<(), TransactionValidityError> { + let (tip, who, imbalance) = pre; + let actual_fee = pallet_transaction_payment::Module::::compute_actual_fee( + len as u32, + info, + post_info, + tip, + ); + ::OnChargeTransaction::correct_and_deposit_fee(&who, info, post_info, actual_fee, tip, imbalance)?; + Ok(()) + } +} \ No newline at end of file --- /dev/null +++ b/pallets/nft-transaction-payment/Cargo.toml @@ -0,0 +1,57 @@ +[package] +authors = ['Substrate DevHub '] +description = 'Unqiue pallet nft specific transaction payment' +edition = '2018' +homepage = 'https://substrate.io' +license = 'Unlicense' +name = 'pallet-nft-transaction-payment' +repository = 'https://github.com/usetech-llc/nft_private/' +version = '3.0.0' + +[package.metadata.docs.rs] +targets = ['x86_64-unknown-linux-gnu'] + +# alias "parity-scale-code" to "codec" +[dependencies.codec] +default-features = false +features = ['derive'] +package = 'parity-scale-codec' +version = '2.0.0' + +[dependencies] +serde = { version = "1.0.119" } +frame-support = { default-features = false, version = '3.0.0' } +frame-system = { default-features = false, version = '3.0.0' } +pallet-balances = { default-features = false, version = '3.0.0' } +pallet-contracts = { default-features = false, version = '3.0.0' } +pallet-timestamp = { default-features = false, version = '3.0.0' } +pallet-transaction-payment = { default-features = false, version = "3.0.0" } +pallet-randomness-collective-flip = { default-features = false, version = '3.0.0' } +sp-std = { default-features = false, version = '3.0.0' } +frame-benchmarking = { default-features = false, version = "3.0.0", optional = true } +sp-core = { default-features = false, version = '3.0.0' } +sp-io = { default-features = false, version = '3.0.0' } +sp-runtime = { default-features = false, version = '3.0.0' } + +pallet-nft = { default-features = false, path="../nft" } +nft-data-structs = { default-features = false, path="../../primitives" } + +[features] +default = ['std'] +std = [ + 'codec/std', + 'serde/std', + 'frame-support/std', + 'frame-system/std', + 'pallet-balances/std', + 'pallet-timestamp/std', + 'pallet-randomness-collective-flip/std', + 'pallet-contracts/std', + 'pallet-nft/std', + 'pallet-transaction-payment/std', + 'sp-std/std', + 'sp-runtime/std', + 'nft-data-structs/std', + 'frame-benchmarking/std', +] +runtime-benchmarks = ["frame-benchmarking"] --- /dev/null +++ b/pallets/nft-transaction-payment/src/benchmarking.rs @@ -0,0 +1,19 @@ +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use crate::Module as NftTransactionPayment; + +use sp_std::prelude::*; +use frame_system::RawOrigin; +use frame_benchmarking::{benchmarks}; +use frame_support::traits::OnInitialize; + +benchmarks! { + + // on_initialize { + // let block1: T::BlockNumber = T::BlockNumber::from(1u32); + // let block2: T::BlockNumber = T::BlockNumber::from(2u32); + // Inflation::::on_initialize(block1); // Create Treasury account + // }: { Inflation::::on_initialize(block2); } // Benchmark deposit_into_existing path + +} --- /dev/null +++ b/pallets/nft-transaction-payment/src/lib.rs @@ -0,0 +1,525 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "std")] +pub use std::*; + +#[cfg(feature = "std")] +pub use serde::*; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +#[cfg(test)] +mod tests; + +use frame_support::{ + decl_error, decl_module, decl_storage, + traits::{ + IsSubType, + }, + weights::{ + DispatchInfo + } +}; +use sp_runtime::traits::StaticLookup; +use sp_runtime::{ + traits::{ + Hash, Dispatchable, + }, +}; +use pallet_contracts::chain_extension::UncheckedFrom; +use sp_std::prelude::*; +use nft_data_structs::{ + CreateItemData, + CollectionId, CollectionMode, TokenId +}; + +type CodeHash = ::Hash; + + pub trait Config: frame_system::Config + pallet_contracts::Config + pallet_transaction_payment::Config + pallet_nft::Config { + } + + // Error for non-fungible-token module. + + decl_error! { + /// Error for non-fungible-token module. + pub enum Error for Module { + /// No available class ID + NoAvailableClassId, + /// No available token ID + NoAvailableTokenId, + /// Token(ClassId, TokenId) not found + TokenNotFound, + /// Class not found + CollectionNotFound, + /// The operator is not the owner of the token and has no permission + NoPermission, + /// Arithmetic calculation overflow + NumOverflow, + /// Can not destroy class + /// Total issuance is not 0 + CannotDestroyClass, + } +} + + decl_storage! { + trait Store for Module as NftTransactionPayment{ + + } +} + +decl_module! { + + pub struct Module for enum Call + where + origin: T::Origin, + { + } +} + +impl Module +{ + + pub fn withdraw_type( + who: &T::AccountId, + call: &T::Call + ) -> Option where + T::Call: Dispatchable, + T::Call: IsSubType>, + T::Call: IsSubType>, + T::AccountId: AsRef<[u8]>, + T::AccountId: UncheckedFrom + { + + let mut sponsor: Option = match IsSubType::>::is_sub_type(call) { + Some(pallet_nft::Call::create_item(collection_id, _owner, _properties)) => { + + Self::withdraw_create_item(who, collection_id, &_properties) + }, + Some(pallet_nft::Call::transfer(_new_owner, collection_id, item_id, _value)) => { + + Self::withdraw_transfer(who, collection_id, item_id) + }, + Some(pallet_nft::Call::set_variable_meta_data(collection_id, item_id, data)) => { + + Self::withdraw_set_variable_meta_data(who, collection_id, item_id, &data) + }, + _ => None, + }; + + sponsor = sponsor.or_else(|| match IsSubType::>::is_sub_type(call) { + Some(pallet_contracts::Call::call(dest, _value, _gas_limit, _data)) => { + + Self::withdraw_contract_call(who, dest) + }, + Some(pallet_contracts::Call::instantiate(_endowment, _gas_limit, code_hash, _data, salt)) => { + + Self::withdraw_contract_instantiate(&who, code_hash, salt) + }, + Some(pallet_contracts::Call::instantiate_with_code(_endowment, _gas_limit, _code, _data, _salt)) => { + + Self::withdraw_contract_instantiate(&who, &T::Hashing::hash(&_code), _salt) + }, + _ => None, + }); + + sponsor + } + + + + pub fn withdraw_create_item( + who: &T::AccountId, + collection_id: &CollectionId, + _properties: &CreateItemData, + ) -> Option { + + let collection = pallet_nft::CollectionById::::get(collection_id)?; + + // sponsor timeout + let block_number = >::block_number() as T::BlockNumber; + + let limit = collection.limits.sponsor_transfer_timeout; + if pallet_nft::CreateItemBasket::::contains_key((collection_id, &who)) { + let last_tx_block = pallet_nft::CreateItemBasket::::get((collection_id, &who)); + let limit_time = last_tx_block + limit.into(); + if block_number <= limit_time { + return None; + } + } + pallet_nft::CreateItemBasket::::insert((collection_id, who.clone()), block_number); + + // check free create limit + if collection.limits.sponsored_data_size >= (_properties.len() as u32) { + collection.sponsorship.sponsor() + .cloned() + } else { + None + } + } + + pub fn withdraw_transfer( + who: &T::AccountId, + collection_id: &CollectionId, + item_id: &TokenId, + ) -> Option { + + let collection = pallet_nft::CollectionById::::get(collection_id)?; + let limits = pallet_nft::ChainLimit::get(); + + let mut sponsor_transfer = false; + if collection.sponsorship.confirmed() { + + let collection_limits = collection.limits.clone(); + let collection_mode = collection.mode.clone(); + + // sponsor timeout + let block_number = >::block_number() as T::BlockNumber; + sponsor_transfer = match collection_mode { + CollectionMode::NFT => { + + // get correct limit + let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 { + collection_limits.sponsor_transfer_timeout + } else { + limits.nft_sponsor_transfer_timeout + }; + + let mut sponsored = true; + if pallet_nft::NftTransferBasket::::contains_key(collection_id, item_id) { + let last_tx_block = pallet_nft::NftTransferBasket::::get(collection_id, item_id); + let limit_time = last_tx_block + limit.into(); + if block_number <= limit_time { + sponsored = false; + } + } + if sponsored { + pallet_nft::NftTransferBasket::::insert(collection_id, item_id, block_number); + } + + sponsored + } + CollectionMode::Fungible(_) => { + + // get correct limit + let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 { + collection_limits.sponsor_transfer_timeout + } else { + limits.fungible_sponsor_transfer_timeout + }; + + let block_number = >::block_number() as T::BlockNumber; + let mut sponsored = true; + if pallet_nft::FungibleTransferBasket::::contains_key(collection_id, who) { + let last_tx_block = pallet_nft::FungibleTransferBasket::::get(collection_id, who); + let limit_time = last_tx_block + limit.into(); + if block_number <= limit_time { + sponsored = false; + } + } + if sponsored { + pallet_nft::FungibleTransferBasket::::insert(collection_id, who, block_number); + } + + sponsored + } + CollectionMode::ReFungible => { + + // get correct limit + let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 { + collection_limits.sponsor_transfer_timeout + } else { + limits.refungible_sponsor_transfer_timeout + }; + + let mut sponsored = true; + if pallet_nft::ReFungibleTransferBasket::::contains_key(collection_id, item_id) { + let last_tx_block = pallet_nft::ReFungibleTransferBasket::::get(collection_id, item_id); + let limit_time = last_tx_block + limit.into(); + if block_number <= limit_time { + sponsored = false; + } + } + if sponsored { + pallet_nft::ReFungibleTransferBasket::::insert(collection_id, item_id, block_number); + } + + sponsored + } + _ => { + false + }, + }; + } + + if !sponsor_transfer { + None + } else { + collection.sponsorship.sponsor() + .cloned() + } + } + + pub fn withdraw_set_variable_meta_data( + who: &T::AccountId, + collection_id: &CollectionId, + item_id: &TokenId, + data: &Vec, + ) -> Option { + + let mut sponsor_metadata_changes = false; + + let collection = pallet_nft::CollectionById::::get(collection_id)?; + + if + collection.sponsorship.confirmed() && + // Can't sponsor fungible collection, this tx will be rejected + // as invalid + !matches!(collection.mode, CollectionMode::Fungible(_)) && + data.len() <= collection.limits.sponsored_data_size as usize + { + if let Some(rate_limit) = collection.limits.sponsored_data_rate_limit { + let block_number = >::block_number() as T::BlockNumber; + + if pallet_nft::VariableMetaDataBasket::::get(collection_id, item_id) + .map(|last_block| block_number - last_block > rate_limit) + .unwrap_or(true) + { + sponsor_metadata_changes = true; + pallet_nft::VariableMetaDataBasket::::insert(collection_id, item_id, block_number); + } + } + } + + if !sponsor_metadata_changes { + None + } else { + collection.sponsorship.sponsor().cloned() + } + + } + + pub fn withdraw_contract_call( + who: &T::AccountId, + dest: &::Source + ) -> Option { + + let called_contract: T::AccountId = T::Lookup::lookup((dest).clone()).unwrap_or(T::AccountId::default()); + + let owned_contract = pallet_nft::ContractOwner::::get(called_contract.clone()).as_ref() == Some(who); + let white_list_enabled = pallet_nft::ContractWhiteListEnabled::::contains_key(called_contract.clone()); + + // ??? + if !owned_contract && white_list_enabled { + if !pallet_nft::ContractWhiteList::::contains_key(called_contract.clone(), who) { + return Some(who.clone()) + // return Err(InvalidTransaction::Call.into()); + } + } + + let mut sponsor_transfer = false; + if pallet_nft::ContractSponsoringRateLimit::::contains_key(called_contract.clone()) { + let last_tx_block = pallet_nft::ContractSponsorBasket::::get((&called_contract, &who)); + let block_number = >::block_number() as T::BlockNumber; + let rate_limit = pallet_nft::ContractSponsoringRateLimit::::get(&called_contract); + let limit_time = last_tx_block + rate_limit; + + if block_number >= limit_time { + pallet_nft::ContractSponsorBasket::::insert((called_contract.clone(), who.clone()), block_number); + sponsor_transfer = true; + } + } else { + sponsor_transfer = false; + } + + if sponsor_transfer { + if pallet_nft::ContractSelfSponsoring::::contains_key(called_contract.clone()) { + if pallet_nft::ContractSelfSponsoring::::get(called_contract.clone()) { + return Some(called_contract); + } + } + } + + None + } + + pub fn withdraw_contract_instantiate( + who: &T::AccountId, + code_hash: &CodeHash, + salt: &[u8], + ) -> Option where + T::AccountId: AsRef<[u8]>, + T::AccountId: UncheckedFrom + { + + let new_contract_address = >::contract_address( + &who, + code_hash, + salt, + ); + pallet_nft::ContractOwner::::insert(new_contract_address.clone(), who.clone()); + + None + } +} + +// type BalanceOf = <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::Balance; + +// /// Require the transactor pay for themselves and maybe include a tip to gain additional priority +// /// in the queue. +// #[derive(Encode, Decode, Clone, Eq, PartialEq)] +// pub struct ChargeTransactionPayment(#[codec(compact)] BalanceOf); + +// impl sp_std::fmt::Debug +// for ChargeTransactionPayment +// { +// #[cfg(feature = "std")] +// fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { +// write!(f, "ChargeTransactionPayment<{:?}>", self.0) +// } +// #[cfg(not(feature = "std"))] +// fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { +// Ok(()) +// } +// } + +// impl ChargeTransactionPayment +// where +// T::Call: Dispatchable + IsSubType> + IsSubType>, +// BalanceOf: Send + Sync + From + FixedPointOperand, +// T::AccountId: AsRef<[u8]>, +// T::AccountId: UncheckedFrom, +// { +// fn traditional_fee( +// len: usize, +// info: &DispatchInfoOf, +// tip: BalanceOf, +// ) -> BalanceOf +// where +// T::Call: Dispatchable, +// { +// >::compute_fee(len as u32, info, tip) +// } + +// fn get_priority(len: usize, info: &DispatchInfoOf, final_fee: BalanceOf) -> TransactionPriority { +// let weight_saturation = T::BlockWeights::get().max_block / info.weight.max(1); +// let max_block_length = *T::BlockLength::get().max.get(DispatchClass::Normal); +// let len_saturation = max_block_length as u64 / (len as u64).max(1); +// let coefficient: BalanceOf = weight_saturation +// .min(len_saturation) +// .saturated_into::>(); +// final_fee +// .saturating_mul(coefficient) +// .saturated_into::() +// } + +// fn withdraw_fee( +// &self, +// who: &T::AccountId, +// call: &T::Call, +// info: &DispatchInfoOf, +// len: usize, +// ) -> Result< +// ( +// BalanceOf, +// <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::LiquidityInfo, +// ), +// TransactionValidityError, +// > { +// let tip = self.0; + +// let fee = Self::traditional_fee(len, info, tip); + +// // Only mess with balances if fee is not zero. +// if fee.is_zero() { +// return <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::withdraw_fee(who, call, info, fee, tip) +// .map(|i| (fee, i)); +// } + +// // 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::::withdraw_type(who, call); +// //let sponsor = Self::Module::::withdraw_type(who, call);; +// // >::withdraw_type(who, call); + +// let who_pays_fee = sponsor.unwrap_or_else(|| who.clone()); + +// <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::withdraw_fee(&who_pays_fee, call, info, fee, tip) +// .map(|i| (fee, i)) +// } +// } + + +// impl SignedExtension +// for ChargeTransactionPayment +// where +// BalanceOf: Send + Sync + From + FixedPointOperand, +// T::Call: Dispatchable + IsSubType> + IsSubType>, +// T::AccountId: AsRef<[u8]>, +// T::AccountId: UncheckedFrom, +// { +// const IDENTIFIER: &'static str = "ChargeTransactionPayment"; +// type AccountId = T::AccountId; +// type Call = T::Call; +// type AdditionalSigned = (); +// type Pre = ( +// // tip +// BalanceOf, +// // who pays fee +// Self::AccountId, +// // imbalance resulting from withdrawing the fee +// <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::LiquidityInfo, +// ); +// fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> { +// Ok(()) +// } + +// fn validate( +// &self, +// who: &Self::AccountId, +// call: &Self::Call, +// info: &DispatchInfoOf, +// len: usize, +// ) -> TransactionValidity { +// let (fee, _) = self.withdraw_fee(who, call, info, len)?; +// Ok(ValidTransaction { +// priority: Self::get_priority(len, info, fee), +// ..Default::default() +// }) +// } + +// fn pre_dispatch( +// self, +// who: &Self::AccountId, +// call: &Self::Call, +// info: &DispatchInfoOf, +// len: usize, +// ) -> Result { +// let (_fee, imbalance) = self.withdraw_fee(who, call, info, len)?; +// Ok((self.0, who.clone(), imbalance)) +// } + +// fn post_dispatch( +// pre: Self::Pre, +// info: &DispatchInfoOf, +// post_info: &PostDispatchInfoOf, +// len: usize, +// _result: &DispatchResult, +// ) -> Result<(), TransactionValidityError> { +// let (tip, who, imbalance) = pre; +// let actual_fee = pallet_transaction_payment::Module::::compute_actual_fee( +// len as u32, +// info, +// post_info, +// tip, +// ); +// ::OnChargeTransaction::correct_and_deposit_fee(&who, info, post_info, actual_fee, tip, imbalance)?; +// Ok(()) +// } +// } \ No newline at end of file --- /dev/null +++ b/pallets/nft-transaction-payment/src/tests.rs @@ -0,0 +1,241 @@ +#[cfg(test)] +mod tests { + use crate as pallet_inflation; + + use frame_system; + use frame_support::{traits::{Currency}, parameter_types}; + use frame_support::{traits::OnInitialize}; + use sp_core::H256; + use sp_runtime::{traits::{BlakeTwo256, IdentityLookup}, testing::Header}; + + type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; + type Block = frame_system::mocking::MockBlock; + + const YEAR: u64 = 5_259_600; + + parameter_types! { + pub const ExistentialDeposit: u64 = 1; + pub const MaxLocks: u32 = 50; + } + + impl pallet_balances::Config for Test { + type AccountStore = System; + type Balance = u64; + type DustRemoval = (); + type Event = (); + type ExistentialDeposit = ExistentialDeposit; + type WeightInfo = (); + type MaxLocks = MaxLocks; + } + + frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + Balances: pallet_balances::{Module, Call, Storage}, + System: frame_system::{Module, Call, Config, Storage, Event}, + Inflation: pallet_inflation::{Module, Call, Storage}, + } + ); + + parameter_types! { + pub const BlockHashCount: u64 = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(1024); + pub const SS58Prefix: u8 = 42; + } + + impl frame_system::Config for Test { + type BaseCallFilter = (); + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = (); + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + } + + parameter_types! { + pub TreasuryAccountId: u64 = 1234; + pub const InflationBlockInterval: u32 = 100; // every time per how many blocks inflation is applied + } + + impl pallet_inflation::Config for Test { + type Currency = Balances; + type TreasuryAccountId = TreasuryAccountId; + type InflationBlockInterval = InflationBlockInterval; + } + + // Build genesis storage according to the mock runtime. + pub fn new_test_ext() -> sp_io::TestExternalities { + frame_system::GenesisConfig::default().build_storage::().unwrap().into() + } + + #[test] + fn inflation_works() { + new_test_ext().execute_with(|| { + // Total issuance = 1_000_000_000 + let initial_issuance: u64 = 1_000_000_000; + let _ = >::deposit_creating(&1234, initial_issuance); + assert_eq!(Balances::free_balance(1234), initial_issuance); + + // BlockInflation should be set after 1st block and + // first inflation deposit should be equal to BlockInflation + Inflation::on_initialize(1); + assert!(Inflation::block_inflation() > 0); + assert_eq!(Balances::free_balance(1234) - initial_issuance, Inflation::block_inflation()); + }); + } + + #[test] + fn inflation_second_deposit() { + new_test_ext().execute_with(|| { + // Total issuance = 1_000_000_000 + let initial_issuance: u64 = 1_000_000_000; + let _ = >::deposit_creating(&1234, initial_issuance); + assert_eq!(Balances::free_balance(1234), initial_issuance); + Inflation::on_initialize(1); + + // Next inflation deposit happens when block is multiple of InflationBlockInterval + let mut block: u32 = 2; + let balance_before: u64 = Balances::free_balance(1234); + while block % InflationBlockInterval::get() != 0 { + Inflation::on_initialize(block as u64); + block += 1; + } + let balance_just_before: u64 = Balances::free_balance(1234); + assert_eq!(balance_before, balance_just_before); + + // The block with inflation + Inflation::on_initialize(block as u64); + let balance_after: u64 = Balances::free_balance(1234); + assert_eq!(balance_after - balance_just_before, Inflation::block_inflation()); + }); + } + + #[test] + fn inflation_in_1_year() { + new_test_ext().execute_with(|| { + // Total issuance = 1_000_000_000 + let initial_issuance: u64 = 1_000_000_000; + let _ = >::deposit_creating(&1234, initial_issuance); + assert_eq!(Balances::free_balance(1234), initial_issuance); + Inflation::on_initialize(1); + let block_inflation_year_0 = Inflation::block_inflation(); + + Inflation::on_initialize(YEAR); + let block_inflation_year_1 = Inflation::block_inflation(); + + // Assert that year 1 inflation is less than year 0 + assert!(block_inflation_year_0 > block_inflation_year_1); + }); + } + + #[test] + fn inflation_in_1_to_9_years() { + new_test_ext().execute_with(|| { + // Total issuance = 1_000_000_000 + let initial_issuance: u64 = 1_000_000_000; + let _ = >::deposit_creating(&1234, initial_issuance); + assert_eq!(Balances::free_balance(1234), initial_issuance); + Inflation::on_initialize(1); + + for year in 1..=9 { + let block_inflation_year_before = Inflation::block_inflation(); + Inflation::on_initialize(YEAR * year); + let block_inflation_year_after = Inflation::block_inflation(); + + // Assert that next year inflation is less than previous year inflation + assert!(block_inflation_year_before > block_inflation_year_after); + } + + }); + } + + #[test] + fn inflation_after_year_10_is_flat() { + new_test_ext().execute_with(|| { + // Total issuance = 1_000_000_000 + let initial_issuance: u64 = 1_000_000_000; + let _ = >::deposit_creating(&1234, initial_issuance); + assert_eq!(Balances::free_balance(1234), initial_issuance); + Inflation::on_initialize(YEAR * 9); + + for year in 10..=20 { + let block_inflation_year_before = Inflation::block_inflation(); + Inflation::on_initialize(YEAR * year); + let block_inflation_year_after = Inflation::block_inflation(); + + // Assert that next year inflation is equal to previous year inflation + assert_eq!(block_inflation_year_before, block_inflation_year_after); + } + }); + } + + #[test] + fn inflation_rate_by_year() { + new_test_ext().execute_with(|| { + let payouts: u64 = YEAR / InflationBlockInterval::get() as u64; + + // Inflation starts at 10% and does down by 2/3% every year until year 9 (included), + // then it is flat. + let payout_by_year: [u64; 11] = [ + 1000, + 933, + 867, + 800, + 733, + 667, + 600, + 533, + 467, + 400, + 400 + ]; + + // For accuracy total issuance = payout0 * payouts * 10; + let initial_issuance: u64 = payout_by_year[0] * payouts * 10; + let _ = >::deposit_creating(&1234, initial_issuance); + assert_eq!(Balances::free_balance(1234), initial_issuance); + + for year in 0..=10 { + // Year first block + Inflation::on_initialize(year*YEAR); + let mut actual_payout = Inflation::block_inflation(); + assert_eq!(actual_payout, payout_by_year[year as usize]); + + // Year second block + Inflation::on_initialize(year*YEAR+1); + actual_payout = Inflation::block_inflation(); + assert_eq!(actual_payout, payout_by_year[year as usize]); + + // Year middle block + Inflation::on_initialize(year*YEAR + YEAR/2); + actual_payout = Inflation::block_inflation(); + assert_eq!(actual_payout, payout_by_year[year as usize]); + + // Year last block + Inflation::on_initialize((year + 1)*YEAR-1); + actual_payout = Inflation::block_inflation(); + assert_eq!(actual_payout, payout_by_year[year as usize]); + } + }); + } +} --- a/pallets/nft/Cargo.toml +++ b/pallets/nft/Cargo.toml @@ -32,6 +32,8 @@ sp-runtime = { default-features = false, version = '3.0.0' } pallet-contracts = { default-features = false, version = '3.0.0' } pallet-transaction-payment = { default-features = false, version = "3.0.0" } +nft-data-structs = { default-features = false, path="../../primitives" } + [features] default = ['std'] @@ -45,6 +47,7 @@ 'pallet-contracts/std', 'pallet-randomness-collective-flip/std', 'pallet-transaction-payment/std', + 'nft-data-structs/std', 'sp-std/std', 'sp-runtime/std', 'frame-benchmarking/std', --- a/pallets/nft/src/lib.rs +++ b/pallets/nft/src/lib.rs @@ -13,8 +13,6 @@ #[cfg(feature = "std")] pub use serde::*; -use core::ops::{Deref, DerefMut}; -use codec::{Decode, Encode}; pub use frame_support::{ construct_runtime, decl_event, decl_module, decl_storage, decl_error, dispatch::DispatchResult, @@ -34,18 +32,13 @@ use frame_system::{self as system, ensure_signed, ensure_root}; use sp_runtime::sp_std::prelude::Vec; -use sp_runtime::{ - traits::{ - Hash, DispatchInfoOf, Dispatchable, PostDispatchInfoOf, Saturating, SaturatedConversion, SignedExtension, Zero, - }, - transaction_validity::{ - TransactionPriority, InvalidTransaction, TransactionValidity, TransactionValidityError, ValidTransaction, - }, - FixedPointOperand, FixedU128, +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, + SchemaVersion, SponsorshipState, Ownership, + NftItemType, FungibleItemType, ReFungibleItemType }; -use sp_runtime::traits::StaticLookup; -use pallet_contracts::chain_extension::UncheckedFrom; -use pallet_transaction_payment::OnChargeTransaction; #[cfg(test)] mod mock; @@ -54,233 +47,9 @@ mod tests; mod default_weights; - -pub const MAX_DECIMAL_POINTS: DecimalPoints = 30; -pub const MAX_REFUNGIBLE_PIECES: u128 = 1_000_000_000_000_000_000_000; -pub const MAX_SPONSOR_TIMEOUT: u32 = 10_368_000; -pub const MAX_TOKEN_OWNERSHIP: u32 = 10_000_000; - -// Structs -// #region - -pub type CollectionId = u32; -pub type TokenId = u32; -pub type DecimalPoints = u8; - -#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub enum CollectionMode { - Invalid, - NFT, - // decimal points - Fungible(DecimalPoints), - ReFungible, -} - -impl Default for CollectionMode { - fn default() -> Self { - Self::Invalid - } -} -impl Into for CollectionMode { - fn into(self) -> u8 { - match self { - CollectionMode::Invalid => 0, - CollectionMode::NFT => 1, - CollectionMode::Fungible(_) => 2, - CollectionMode::ReFungible => 3, - } - } -} - -#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub enum AccessMode { - Normal, - WhiteList, -} -impl Default for AccessMode { - fn default() -> Self { - Self::Normal - } -} - -#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub enum SchemaVersion { - ImageURL, - Unique, -} -impl Default for SchemaVersion { - fn default() -> Self { - Self::ImageURL - } -} - -#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct Ownership { - pub owner: AccountId, - pub fraction: u128, -} - -#[derive(Encode, Decode, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub enum SponsorshipState { - /// The fees are applied to the transaction sender - Disabled, - Unconfirmed(AccountId), - /// Transactions are sponsored by specified account - Confirmed(AccountId), -} - -impl SponsorshipState { - fn sponsor(&self) -> Option<&AccountId> { - match self { - Self::Confirmed(sponsor) => Some(sponsor), - _ => None, - } - } - - fn pending_sponsor(&self) -> Option<&AccountId> { - match self { - Self::Unconfirmed(sponsor) | Self::Confirmed(sponsor) => Some(sponsor), - _ => None, - } - } - - fn confirmed(&self) -> bool { - matches!(self, Self::Confirmed(_)) - } -} - -impl Default for SponsorshipState { - fn default() -> Self { - Self::Disabled - } -} - -#[derive(Encode, Decode, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct Collection { - pub owner: T::AccountId, - pub mode: CollectionMode, - pub access: AccessMode, - pub decimal_points: DecimalPoints, - pub name: Vec, // 64 include null escape char - pub description: Vec, // 256 include null escape char - pub token_prefix: Vec, // 16 include null escape char - pub mint_mode: bool, - pub offchain_schema: Vec, - pub schema_version: SchemaVersion, - pub sponsorship: SponsorshipState, - pub limits: CollectionLimits, // Collection private restrictions - pub variable_on_chain_schema: Vec, // - pub const_on_chain_schema: Vec, // -} - -pub struct CollectionHandle { - pub id: CollectionId, - collection: Collection, -} - -impl Deref for CollectionHandle { - type Target = Collection; - - fn deref(&self) -> &Self::Target { - &self.collection - } -} - -impl DerefMut for CollectionHandle { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.collection - } -} - -#[derive(Encode, Decode, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct NftItemType { - pub owner: AccountId, - pub const_data: Vec, - pub variable_data: Vec, -} - -#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct FungibleItemType { - pub value: u128, -} - -#[derive(Encode, Decode, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct ReFungibleItemType { - pub owner: Vec>, - pub const_data: Vec, - pub variable_data: Vec, -} - -// #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] -// #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -// pub struct VestingItem { -// pub sender: AccountId, -// pub recipient: AccountId, -// pub collection_id: CollectionId, -// pub item_id: TokenId, -// pub amount: u64, -// pub vesting_date: Moment, -// } - -#[derive(Encode, Decode, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct CollectionLimits { - pub account_token_ownership_limit: u32, - pub sponsored_data_size: u32, - /// None - setVariableMetadata is not sponsored - /// Some(v) - setVariableMetadata is sponsored - /// if there is v block between txs - pub sponsored_data_rate_limit: Option, - pub token_limit: u32, - - // Timeouts for item types in passed blocks - pub sponsor_transfer_timeout: u32, - pub owner_can_transfer: bool, - pub owner_can_destroy: bool, -} - -impl Default for CollectionLimits { - fn default() -> Self { - Self { - account_token_ownership_limit: 10_000_000, - token_limit: u32::max_value(), - sponsored_data_size: u32::MAX, - sponsored_data_rate_limit: None, - sponsor_transfer_timeout: 14400, - owner_can_transfer: true, - owner_can_destroy: true - } - } -} - -#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct ChainLimits { - pub collection_numbers_limit: u32, - pub account_token_ownership_limit: u32, - pub collections_admins_limit: u64, - pub custom_data_limit: u32, - - // Timeouts for item types in passed blocks - pub nft_sponsor_transfer_timeout: u32, - pub fungible_sponsor_transfer_timeout: u32, - pub refungible_sponsor_transfer_timeout: u32, - - // Schema limits - pub offchain_schema_limit: u32, - pub variable_on_chain_schema_limit: u32, - pub const_on_chain_schema_limit: u32, -} +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; pub trait WeightInfo { fn create_collection() -> Weight; @@ -315,66 +84,6 @@ fn set_collection_limits() -> Weight; } -#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct CreateNftData { - pub const_data: Vec, - pub variable_data: Vec, -} - -#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct CreateFungibleData { - pub value: u128, -} - -#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct CreateReFungibleData { - pub const_data: Vec, - pub variable_data: Vec, - pub pieces: u128, -} - -#[derive(Encode, Decode, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub enum CreateItemData { - NFT(CreateNftData), - Fungible(CreateFungibleData), - ReFungible(CreateReFungibleData), -} - -impl CreateItemData { - pub fn len(&self) -> usize { - let len = match self { - CreateItemData::NFT(data) => data.variable_data.len() + data.const_data.len(), - CreateItemData::ReFungible(data) => data.variable_data.len() + data.const_data.len(), - _ => 0 - }; - - return len; - } -} - -impl From for CreateItemData { - fn from(item: CreateNftData) -> Self { - CreateItemData::NFT(item) - } -} - -impl From for CreateItemData { - fn from(item: CreateReFungibleData) -> Self { - CreateItemData::ReFungible(item) - } -} - -impl From for CreateItemData { - fn from(item: CreateFungibleData) -> Self { - CreateItemData::Fungible(item) - } -} - - decl_error! { /// Error for non-fungible-token module. pub enum Error for Module { @@ -455,7 +164,10 @@ } } -pub trait Config: system::Config + Sized + pallet_transaction_payment::Config + pallet_contracts::Config { +// + pallet_transaction_payment::Config + pallet_nft_transaction_payment::Config + pallet_contracts::Config + + +pub trait Config: system::Config + Sized { type Event: From> + Into<::Event>; /// Weight information for extrinsics in this pallet. @@ -466,11 +178,6 @@ type TreasuryAccountId: Get; } -#[cfg(feature = "runtime-benchmarks")] -mod benchmarking; - -// #endregion - // # Used definitions // // ## User control levels @@ -677,7 +384,7 @@ fn deposit_event() = default; type Error = Error; - fn on_initialize(now: T::BlockNumber) -> Weight { + fn on_initialize(_now: T::BlockNumber) -> Weight { 0 } @@ -2483,404 +2190,7 @@ fn ensure_contract_owned(account: T::AccountId, contract: &T::AccountId) -> DispatchResult { ensure!(>::get(contract) == Some(account), Error::::NoPermission); - - Ok(()) - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////// -// Economic models -// #region - -/// Fee multiplier. -pub type Multiplier = FixedU128; - -type BalanceOf = <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::Balance; - -/// Require the transactor pay for themselves and maybe include a tip to gain additional priority -/// in the queue. -#[derive(Encode, Decode, Clone, Eq, PartialEq)] -pub struct ChargeTransactionPayment(#[codec(compact)] BalanceOf); - -impl sp_std::fmt::Debug - for ChargeTransactionPayment -{ - #[cfg(feature = "std")] - fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { - write!(f, "ChargeTransactionPayment<{:?}>", self.0) - } - #[cfg(not(feature = "std"))] - fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { - Ok(()) - } -} - -impl ChargeTransactionPayment -where - T::Call: Dispatchable + IsSubType> + IsSubType>, - BalanceOf: Send + Sync + From + FixedPointOperand, - T::AccountId: AsRef<[u8]>, - T::AccountId: UncheckedFrom, -{ - fn traditional_fee( - len: usize, - info: &DispatchInfoOf, - tip: BalanceOf, - ) -> BalanceOf - where - T::Call: Dispatchable, - { - >::compute_fee(len as u32, info, tip) - } - - fn get_priority(len: usize, info: &DispatchInfoOf, final_fee: BalanceOf) -> TransactionPriority { - let weight_saturation = T::BlockWeights::get().max_block / info.weight.max(1); - let max_block_length = *T::BlockLength::get().max.get(DispatchClass::Normal); - let len_saturation = max_block_length as u64 / (len as u64).max(1); - let coefficient: BalanceOf = weight_saturation - .min(len_saturation) - .saturated_into::>(); - final_fee - .saturating_mul(coefficient) - .saturated_into::() - } - - fn withdraw_fee( - &self, - who: &T::AccountId, - call: &T::Call, - info: &DispatchInfoOf, - len: usize, - ) -> Result< - ( - BalanceOf, - <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::LiquidityInfo, - ), - TransactionValidityError, - > { - let tip = self.0; - - let fee = Self::traditional_fee(len, info, tip); - - // Only mess with balances if fee is not zero. - if fee.is_zero() { - return <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::withdraw_fee(who, call, info, fee, tip) - .map(|i| (fee, i)); - } - - // Determine who is paying transaction fee based on ecnomic model - // Parse call to extract collection ID and access collection sponsor - let mut sponsor: Option = (|| match IsSubType::>::is_sub_type(call) { - Some(Call::create_item(collection_id, _owner, _properties)) => { - let collection = >::get(collection_id)?; - - // sponsor timeout - let block_number = >::block_number() as T::BlockNumber; - - let limit = collection.limits.sponsor_transfer_timeout; - if >::contains_key((collection_id, &who)) { - let last_tx_block = >::get((collection_id, &who)); - let limit_time = last_tx_block + limit.into(); - if block_number <= limit_time { - return None; - } - } - >::insert((collection_id, who.clone()), block_number); - - // check free create limit - if collection.limits.sponsored_data_size >= (_properties.len() as u32) { - collection.sponsorship.sponsor() - .cloned() - } else { - None - } - } - Some(Call::transfer(_new_owner, collection_id, item_id, _value)) => { - let collection = >::get(collection_id)?; - - let mut sponsor_transfer = false; - if collection.sponsorship.confirmed() { - - let collection_limits = collection.limits; - let collection_mode = collection.mode; - - // sponsor timeout - let block_number = >::block_number() as T::BlockNumber; - sponsor_transfer = match collection_mode { - CollectionMode::NFT => { - - // get correct limit - let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 { - collection_limits.sponsor_transfer_timeout - } else { - ChainLimit::get().nft_sponsor_transfer_timeout - }; - - let mut sponsored = true; - if >::contains_key(collection_id, item_id) { - let last_tx_block = >::get(collection_id, item_id); - let limit_time = last_tx_block + limit.into(); - if block_number <= limit_time { - sponsored = false; - } - } - if sponsored { - >::insert(collection_id, item_id, block_number); - } - - sponsored - } - CollectionMode::Fungible(_) => { - - // get correct limit - let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 { - collection_limits.sponsor_transfer_timeout - } else { - ChainLimit::get().fungible_sponsor_transfer_timeout - }; - - let block_number = >::block_number() as T::BlockNumber; - let mut sponsored = true; - if >::contains_key(collection_id, who) { - let last_tx_block = >::get(collection_id, who); - let limit_time = last_tx_block + limit.into(); - if block_number <= limit_time { - sponsored = false; - } - } - if sponsored { - >::insert(collection_id, who, block_number); - } - - sponsored - } - CollectionMode::ReFungible => { - - // get correct limit - let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 { - collection_limits.sponsor_transfer_timeout - } else { - ChainLimit::get().refungible_sponsor_transfer_timeout - }; - - let mut sponsored = true; - if >::contains_key(collection_id, item_id) { - let last_tx_block = >::get(collection_id, item_id); - let limit_time = last_tx_block + limit.into(); - if block_number <= limit_time { - sponsored = false; - } - } - if sponsored { - >::insert(collection_id, item_id, block_number); - } - - sponsored - } - _ => { - false - }, - }; - } - - if !sponsor_transfer { - None - } else { - collection.sponsorship.sponsor() - .cloned() - } - } - - Some(Call::set_variable_meta_data(collection_id, item_id, data)) => { - let mut sponsor_metadata_changes = false; - - let collection = >::get(collection_id)?; - - if - collection.sponsorship.confirmed() && - // Can't sponsor fungible collection, this tx will be rejected - // as invalid - !matches!(collection.mode, CollectionMode::Fungible(_)) && - data.len() <= collection.limits.sponsored_data_size as usize - { - if let Some(rate_limit) = collection.limits.sponsored_data_rate_limit { - let block_number = >::block_number() as T::BlockNumber; - - if >::get(collection_id, item_id) - .map(|last_block| block_number - last_block > rate_limit) - .unwrap_or(true) - { - sponsor_metadata_changes = true; - >::insert(collection_id, item_id, block_number); - } - } - } - - if !sponsor_metadata_changes { - None - } else { - collection.sponsorship.sponsor().cloned() - } - } - - _ => None, - })(); - - match IsSubType::>::is_sub_type(call) { - Some(pallet_contracts::Call::call(dest, _value, _gas_limit, _data)) => { - - let called_contract: T::AccountId = T::Lookup::lookup((*dest).clone()).unwrap_or(T::AccountId::default()); - - let owned_contract = >::get(called_contract.clone()).as_ref() == Some(who); - let white_list_enabled = >::contains_key(called_contract.clone()); - - if !owned_contract && white_list_enabled { - if !>::contains_key(called_contract.clone(), who) { - return Err(InvalidTransaction::Call.into()); - } - } - }, - _ => {}, - } - - // Sponsor smart contracts - sponsor = sponsor.or_else(|| match IsSubType::>::is_sub_type(call) { - - // On instantiation: set the contract owner - Some(pallet_contracts::Call::instantiate(_endowment, _gas_limit, code_hash, _data, salt)) => { - - let new_contract_address = >::contract_address( - &who, - code_hash, - salt, - ); - >::insert(new_contract_address.clone(), who.clone()); - - None - }, - - // On instantiation with code: set the contract owner - Some(pallet_contracts::Call::instantiate_with_code(_endowment, _gas_limit, _code, _data, _salt)) => { - - let new_contract_address = >::contract_address( - &who, - &T::Hashing::hash(&_code), - _salt, - ); - - >::insert(new_contract_address.clone(), who.clone()); - None - } - - // When the contract is called, check if the sponsoring is enabled and pay fees from contract endowment if it is - Some(pallet_contracts::Call::call(dest, _value, _gas_limit, _data)) => { - - let called_contract: T::AccountId = T::Lookup::lookup((*dest).clone()).unwrap_or(T::AccountId::default()); - - let mut sponsor_transfer = false; - if >::contains_key(called_contract.clone()) { - let last_tx_block = >::get((&called_contract, &who)); - let block_number = >::block_number() as T::BlockNumber; - let rate_limit = >::get(&called_contract); - let limit_time = last_tx_block + rate_limit; - - if block_number >= limit_time { - >::insert((called_contract.clone(), who.clone()), block_number); - sponsor_transfer = true; - } - } else { - sponsor_transfer = false; - } - - if sponsor_transfer { - if >::contains_key(called_contract.clone()) { - if >::get(called_contract.clone()) { - return Some(called_contract); - } - } - } - - None - }, - - _ => None, - }); - - let who_pays_fee = sponsor.unwrap_or_else(|| who.clone()); - - <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::withdraw_fee(&who_pays_fee, call, info, fee, tip) - .map(|i| (fee, i)) - } -} - - -impl SignedExtension - for ChargeTransactionPayment -where - BalanceOf: Send + Sync + From + FixedPointOperand, - T::Call: Dispatchable + IsSubType> + IsSubType>, - T::AccountId: AsRef<[u8]>, - T::AccountId: UncheckedFrom, -{ - const IDENTIFIER: &'static str = "ChargeTransactionPayment"; - type AccountId = T::AccountId; - type Call = T::Call; - type AdditionalSigned = (); - type Pre = ( - // tip - BalanceOf, - // who pays fee - Self::AccountId, - // imbalance resulting from withdrawing the fee - <::OnChargeTransaction as pallet_transaction_payment::OnChargeTransaction>::LiquidityInfo, - ); - fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> { Ok(()) } - - fn validate( - &self, - who: &Self::AccountId, - call: &Self::Call, - info: &DispatchInfoOf, - len: usize, - ) -> TransactionValidity { - let (fee, _) = self.withdraw_fee(who, call, info, len)?; - Ok(ValidTransaction { - priority: Self::get_priority(len, info, fee), - ..Default::default() - }) - } - - fn pre_dispatch( - self, - who: &Self::AccountId, - call: &Self::Call, - info: &DispatchInfoOf, - len: usize, - ) -> Result { - let (_fee, imbalance) = self.withdraw_fee(who, call, info, len)?; - Ok((self.0, who.clone(), imbalance)) - } - - fn post_dispatch( - pre: Self::Pre, - info: &DispatchInfoOf, - post_info: &PostDispatchInfoOf, - len: usize, - _result: &DispatchResult, - ) -> Result<(), TransactionValidityError> { - let (tip, who, imbalance) = pre; - let actual_fee = pallet_transaction_payment::Module::::compute_actual_fee( - len as u32, - info, - post_info, - tip, - ); - ::OnChargeTransaction::correct_and_deposit_fee(&who, info, post_info, actual_fee, tip, imbalance)?; - Ok(()) - } } - -// #endregion \ No newline at end of file --- /dev/null +++ b/pallets/scheduler/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "pallet-scheduler" +version = "3.0.0" +authors = ["Parity Technologies "] +edition = "2018" +license = "Unlicense" +homepage = "https://substrate.dev" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME example pallet" +readme = "README.md" + +[dependencies] +serde = { version = "1.0.119" } +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false } +frame-support = { default-features = false, version = '3.0.0' } +frame-system = { default-features = false, version = '3.0.0' } +pallet-nft-transaction-payment = { default-features = false, path = "../nft-transaction-payment" } +pallet-nft = { default-features = false, path = "../nft" } +nft-data-structs = { path = '../../primitives', default-features = false } + +pallet-contracts = { default-features = false, version = '3.0.0' } +sp-runtime = { default-features = false, version = '3.0.0' } +sp-std = { default-features = false, version = '3.0.0' } +sp-io = { default-features = false, version = '3.0.0' } +log = { version = "0.4.14", default-features = false } + +frame-benchmarking = { default-features = false, version = '3.0.0', optional = true } + +[dev-dependencies] +sp-core = { default-features = false, version = '3.0.0' } +substrate-test-utils = { version = "3.0.0" } + +[features] +default = ["std"] +std = [ + "codec/std", + "sp-runtime/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "pallet-nft-transaction-payment/std", + "pallet-nft/std", + "pallet-contracts/std", + "nft-data-structs/std", + "sp-io/std", + "sp-std/std", + "log/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +#try-runtime = ["frame-support/try-runtime"] --- /dev/null +++ b/pallets/scheduler/README.md @@ -0,0 +1,34 @@ +# Scheduler +A module for scheduling dispatches. + +- [`scheduler::Trait`](https://docs.rs/pallet-scheduler/latest/pallet_scheduler/trait.Trait.html) +- [`Call`](https://docs.rs/pallet-scheduler/latest/pallet_scheduler/enum.Call.html) +- [`Module`](https://docs.rs/pallet-scheduler/latest/pallet_scheduler/struct.Module.html) + +## Overview + +This module exposes capabilities for scheduling dispatches to occur at a +specified block number or at a specified period. These scheduled dispatches +may be named or anonymous and may be canceled. + +**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. + +## Interface + +### Dispatchable Functions + +* `schedule` - schedule a dispatch, which may be periodic, to occur at a + specified block and with a specified priority. +* `cancel` - cancel a scheduled dispatch, specified by block number and + index. +* `schedule_named` - augments the `schedule` interface with an additional + `Vec` parameter that can be used for identification. +* `cancel_named` - the named complement to the cancel function. + +License: Unlicense --- /dev/null +++ b/pallets/scheduler/src/benchmarking.rs @@ -0,0 +1,148 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Scheduler pallet benchmarking. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use sp_std::{vec, prelude::*}; +use frame_system::RawOrigin; +use frame_support::{ensure, traits::OnInitialize}; +use frame_benchmarking::{benchmarks, impl_benchmark_test_suite}; + +use crate::Module as Scheduler; +use frame_system::Pallet as System; + +const BLOCK_NUMBER: u32 = 2; + +// Add `n` named items to the schedule +fn fill_schedule (when: T::BlockNumber, n: u32) -> Result<(), &'static str> { + // Essentially a no-op call. + let call = frame_system::Call::set_storage(vec![]); + for i in 0..n { + // Named schedule is strictly heavier than anonymous + Scheduler::::do_schedule_named( + i.encode(), + DispatchTime::At(when), + // Add periodicity + Some((T::BlockNumber::one(), 100)), + // HARD_DEADLINE priority means it gets executed no matter what + 0, + frame_system::RawOrigin::Root.into(), + call.clone().into(), + )?; + } + ensure!(Agenda::::get(when).len() == n as usize, "didn't fill schedule"); + Ok(()) +} + +benchmarks! { + schedule { + let s in 0 .. T::MaxScheduledPerBlock::get(); + let when = BLOCK_NUMBER.into(); + let periodic = Some((T::BlockNumber::one(), 100)); + let priority = 0; + // Essentially a no-op call. + let call = Box::new(frame_system::Call::set_storage(vec![]).into()); + + fill_schedule::(when, s)?; + }: _(RawOrigin::Root, when, periodic, priority, call) + verify { + ensure!( + Agenda::::get(when).len() == (s + 1) as usize, + "didn't add to schedule" + ); + } + + cancel { + let s in 1 .. T::MaxScheduledPerBlock::get(); + let when = BLOCK_NUMBER.into(); + + fill_schedule::(when, s)?; + assert_eq!(Agenda::::get(when).len(), s as usize); + }: _(RawOrigin::Root, when, 0) + verify { + ensure!( + Lookup::::get(0.encode()).is_none(), + "didn't remove from lookup" + ); + // Removed schedule is NONE + ensure!( + Agenda::::get(when)[0].is_none(), + "didn't remove from schedule" + ); + } + + schedule_named { + let s in 0 .. T::MaxScheduledPerBlock::get(); + let id = s.encode(); + let when = BLOCK_NUMBER.into(); + let periodic = Some((T::BlockNumber::one(), 100)); + let priority = 0; + // Essentially a no-op call. + let call = Box::new(frame_system::Call::set_storage(vec![]).into()); + + fill_schedule::(when, s)?; + }: _(RawOrigin::Root, id, when, periodic, priority, call) + verify { + ensure!( + Agenda::::get(when).len() == (s + 1) as usize, + "didn't add to schedule" + ); + } + + cancel_named { + let s in 1 .. T::MaxScheduledPerBlock::get(); + let when = BLOCK_NUMBER.into(); + + fill_schedule::(when, s)?; + }: _(RawOrigin::Root, 0.encode()) + verify { + ensure!( + Lookup::::get(0.encode()).is_none(), + "didn't remove from lookup" + ); + // Removed schedule is NONE + ensure!( + Agenda::::get(when)[0].is_none(), + "didn't remove from schedule" + ); + } + + // TODO [#7141]: Make this more complex and flexible so it can be used in automation. + #[extra] + on_initialize { + let s in 0 .. T::MaxScheduledPerBlock::get(); + let when = BLOCK_NUMBER.into(); + fill_schedule::(when, s)?; + }: { Scheduler::::on_initialize(BLOCK_NUMBER.into()); } + verify { + assert_eq!(System::::event_count(), s); + // Next block should have all the schedules again + ensure!( + Agenda::::get(when + T::BlockNumber::one()).len() == s as usize, + "didn't append schedule" + ); + } +} + +impl_benchmark_test_suite!( + Scheduler, + crate::tests::new_test_ext(), + crate::tests::Test, +); --- /dev/null +++ b/pallets/scheduler/src/lib.rs @@ -0,0 +1,1611 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Scheduler +//! A module for scheduling dispatches. +//! +//! - [`Config`] +//! - [`Call`] +//! - [`Module`] +//! +//! ## Overview +//! +//! This module exposes capabilities for scheduling dispatches to occur at a +//! specified block number or at a specified period. These scheduled dispatches +//! may be named or anonymous and may be canceled. +//! +//! **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. +//! +//! ## Interface +//! +//! ### Dispatchable Functions +//! +//! * `schedule` - schedule a dispatch, which may be periodic, to occur at a +//! specified block and with a specified priority. +//! * `cancel` - cancel a scheduled dispatch, specified by block number and +//! index. +//! * `schedule_named` - augments the `schedule` interface with an additional +//! `Vec` parameter that can be used for identification. +//! * `cancel_named` - the named complement to the cancel function. + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +mod benchmarking; +pub mod weights; + +use sp_std::{prelude::*, marker::PhantomData, borrow::Borrow}; +use codec::{Encode, Decode, Codec}; +use sp_runtime::{RuntimeDebug, traits::{Zero, One, BadOrigin, Saturating}}; +use frame_support::{ + decl_module, decl_storage, decl_event, decl_error, IterableStorageMap, + dispatch::{Dispatchable, DispatchError, DispatchResult, Parameter}, + traits::{Get, schedule::{self, DispatchTime}, OriginTrait, EnsureOrigin, IsType}, + weights::{GetDispatchInfo, Weight}, +}; +use frame_system::{self as system, ensure_signed}; +use pallet_nft::*; +// use pallet_nft_transaction_payment::{self as nft_transaction_payment}; +use nft_data_structs::*; +pub use weights::WeightInfo; + +/// Our pallet's configuration trait. All our types and constants go in here. If the +/// pallet is dependent on specific other pallets, then their configuration traits +/// should be added to our implied traits list. +/// +/// `system::Config` should always be included in our implied traits. +/// // +pub trait Config: system::Config +{ + + /// The overarching event type. + type Event: From> + Into<::Event>; + + /// The aggregated origin which the dispatch will take. + type Origin: OriginTrait + + From + IsType<::Origin>; + + /// The caller origin, overarching type of all pallets origins. + type PalletsOrigin: From> + Codec + Clone + Eq; + + /// The aggregated call type. + type Call: Parameter + Dispatchable::Origin> + GetDispatchInfo + From>; + + /// The maximum weight that may be scheduled per block for any dispatchables of less priority + /// than `schedule::HARD_DEADLINE`. + type MaximumWeight: Get; + + /// Required origin to schedule or cancel calls. + type ScheduleOrigin: EnsureOrigin<::Origin>; + + /// The maximum number of scheduled calls in the queue for a single block. + /// Not strictly enforced, but used for weight estimation. + type MaxScheduledPerBlock: Get; + + /// TODO + type Sponsoring: SponsoringResolve::Call>; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; +} + +// pub type SelfWeightInfo = ::WeightInfo; + +/// Just a simple index for naming period tasks. +pub type PeriodicIndex = u32; +/// The location of a scheduled task that can be used to remove it. +pub type TaskAddress = (BlockNumber, u32); + +#[cfg_attr(any(feature = "std", test), derive(PartialEq, Eq))] +#[derive(Clone, RuntimeDebug, Encode, Decode)] +struct ScheduledV1 { + maybe_id: Option>, + priority: schedule::Priority, + call: Call, + maybe_periodic: Option>, +} + +/// Information regarding an item to be executed in the future. +#[cfg_attr(any(feature = "std", test), derive(PartialEq, Eq))] +#[derive(Clone, RuntimeDebug, Encode, Decode)] +pub struct ScheduledV2 { + /// The unique identity for this task, if there is one. + maybe_id: Option>, + /// This task's priority. + priority: schedule::Priority, + /// The call to be dispatched. + call: Call, + /// If the call is periodic, then this points to the information concerning that. + maybe_periodic: Option>, + /// The origin to dispatch the call. + origin: PalletsOrigin, + _phantom: PhantomData, +} + +/// The current version of Scheduled struct. +pub type Scheduled = ScheduledV2; + +// A value placed in storage that represents the current version of the Scheduler storage. +// This value is used by the `on_runtime_upgrade` logic to determine whether we run +// storage migration logic. +#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug)] +enum Releases { + V1, + V2, +} + +impl Default for Releases { + fn default() -> Self { + Releases::V1 + } +} + +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)] +pub struct CallSpec { + + module: u32, + method: u32, +} + +decl_storage! { + trait Store for Module as Scheduler { + /// Items to be executed, indexed by the block number that they should be executed on. + pub Agenda: map hasher(twox_64_concat) T::BlockNumber + => Vec::Call, T::BlockNumber, T::PalletsOrigin, T::AccountId>>>; + + pub SpecAgenda: map hasher(twox_64_concat) T::BlockNumber + => Vec>; + + /// Lookup from identity to the block number and index of the task. + Lookup: map hasher(twox_64_concat) Vec => Option>; + + /// Storage version of the pallet. + /// + /// New networks start with last version. + StorageVersion build(|_| Releases::V2): Releases; + } +} + +decl_event!( + pub enum Event where ::BlockNumber { + /// Scheduled some task. \[when, index\] + Scheduled(BlockNumber, u32), + /// Canceled some task. \[when, index\] + Canceled(BlockNumber, u32), + /// Dispatched some task. \[task, id, result\] + Dispatched(TaskAddress, Option>, DispatchResult), + } +); + +decl_error! { + pub enum Error for Module { + /// Failed to schedule a call + FailedToSchedule, + /// Cannot find the scheduled call. + NotFound, + /// Given target block number is in the past. + TargetBlockNumberInPast, + /// Reschedule failed because it does not change scheduled time. + RescheduleNoChange, + } +} + +decl_module! { + /// Scheduler module declaration. + pub struct Module for enum Call + where + origin: ::Origin + { + type Error = Error; + fn deposit_event() = default; + + + /// Anonymously schedule a task. + /// + /// # + /// - S = Number of already scheduled calls + /// - Base Weight: 22.29 + .126 * S µs + /// - DB Weight: + /// - Read: Agenda + /// - Write: Agenda + /// - Will use base weight of 25 which should be good for up to 30 scheduled calls + /// # + // #[weight = ::WeightInfo::schedule(T::MaxScheduledPerBlock::get())] + #[weight = 0] + fn schedule(origin, + when: T::BlockNumber, + maybe_periodic: Option>, + priority: schedule::Priority, + call: Box<::Call>, + ) + { + let origin = ::Origin::from(origin); + Self::do_schedule(DispatchTime::At(when), maybe_periodic, priority, origin.caller().clone(), *call)?; + } + + /// Cancel an anonymously scheduled task. + /// + /// # + /// - S = Number of already scheduled calls + /// - Base Weight: 22.15 + 2.869 * S µs + /// - DB Weight: + /// - Read: Agenda + /// - Write: Agenda, Lookup + /// - Will use base weight of 100 which should be good for up to 30 scheduled calls + /// # + #[weight = ::WeightInfo::cancel(T::MaxScheduledPerBlock::get())] + fn cancel(origin, when: T::BlockNumber, index: u32) { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + let origin = ::Origin::from(origin); + Self::do_cancel(Some(origin.caller().clone()), (when, index))?; + } + + /// Schedule a named task. + /// + /// # + /// - S = Number of already scheduled calls + /// - Base Weight: 29.6 + .159 * S µs + /// - DB Weight: + /// - Read: Agenda, Lookup + /// - Write: Agenda, Lookup + /// - Will use base weight of 35 which should be good for more than 30 scheduled calls + /// # + #[weight = ::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get())] + fn schedule_named(origin, + id: Vec, + when: T::BlockNumber, + maybe_periodic: Option>, + priority: schedule::Priority, + call: Box<::Call>, + ) { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + let origin = ::Origin::from(origin); + Self::do_schedule_named( + id, DispatchTime::At(when), maybe_periodic, priority, origin.caller().clone(), *call + )?; + } + + /// Cancel a named scheduled task. + /// + /// # + /// - S = Number of already scheduled calls + /// - Base Weight: 24.91 + 2.907 * S µs + /// - DB Weight: + /// - Read: Agenda, Lookup + /// - Write: Agenda, Lookup + /// - Will use base weight of 100 which should be good for up to 30 scheduled calls + /// # + #[weight = ::WeightInfo::cancel_named(T::MaxScheduledPerBlock::get())] + fn cancel_named(origin, id: Vec) { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + let origin = ::Origin::from(origin); + Self::do_cancel_named(Some(origin.caller().clone()), id)?; + } + + /// Anonymously schedule a task after a delay. + /// + /// # + /// Same as [`schedule`]. + /// # + #[weight = ::WeightInfo::schedule(T::MaxScheduledPerBlock::get())] + fn schedule_after(origin, + after: T::BlockNumber, + maybe_periodic: Option>, + priority: schedule::Priority, + call: Box<::Call>, + ) { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + let origin = ::Origin::from(origin); + Self::do_schedule( + DispatchTime::After(after), maybe_periodic, priority, origin.caller().clone(), *call + )?; + } + + /// Schedule a named task after a delay. + /// + /// # + /// Same as [`schedule_named`]. + /// # + #[weight = ::WeightInfo::schedule_named(T::MaxScheduledPerBlock::get())] + fn schedule_named_after(origin, + id: Vec, + after: T::BlockNumber, + maybe_periodic: Option>, + priority: schedule::Priority, + call: Box<::Call>, + ) { + T::ScheduleOrigin::ensure_origin(origin.clone())?; + let origin = ::Origin::from(origin); + Self::do_schedule_named( + id, DispatchTime::After(after), maybe_periodic, priority, origin.caller().clone(), *call + )?; + } + + /// Execute the scheduled calls + /// + /// # + /// - S = Number of already scheduled calls + /// - N = Named scheduled calls + /// - P = Periodic Calls + /// - Base Weight: 9.243 + 23.45 * S µs + /// - DB Weight: + /// - Read: Agenda + Lookup * N + Agenda(Future) * P + /// - Write: Agenda + Lookup * N + Agenda(future) * P + /// # + fn on_initialize(now: T::BlockNumber) -> Weight { + let limit = T::MaximumWeight::get(); + let mut queued = Agenda::::take(now).into_iter() + .enumerate() + .filter_map(|(index, s)| s.map(|inner| (index as u32, inner))) + .collect::>(); + if queued.len() as u32 > T::MaxScheduledPerBlock::get() { + log::warn!( + target: "runtime::scheduler", + "Warning: This block has more items queued in Scheduler than \ + expected from the runtime configuration. An update might be needed." + ); + } + 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 = ::WeightInfo::schedule(T::MaxScheduledPerBlock::get()); + queued.into_iter() + .enumerate() + .scan(base_weight, |cumulative_weight, (order, (index, s))| { + *cumulative_weight = cumulative_weight + .saturating_add(s.call.get_dispatch_info().weight); + + let origin = <::Origin as From>::from( + s.origin.clone() + ).into(); + + if ensure_signed(origin).is_ok() { + // AccountData for inner call origin accountdata. + *cumulative_weight = cumulative_weight + .saturating_add(T::DbWeight::get().reads_writes(1, 1)); + } + + if s.maybe_id.is_some() { + // Remove/Modify Lookup + *cumulative_weight = cumulative_weight.saturating_add(T::DbWeight::get().writes(1)); + } + if s.maybe_periodic.is_some() { + // Read/Write Agenda for future block + *cumulative_weight = cumulative_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1)); + } + + Some((order, index, *cumulative_weight, s)) + }) + .filter_map(|(order, index, cumulative_weight, mut s)| { + // We allow a scheduled call if any is true: + // - It's priority is `HARD_DEADLINE` + // - It does not push the weight past the limit. + // - It is the first item in the schedule + if s.priority <= schedule::HARD_DEADLINE || cumulative_weight <= limit || order == 0 { + + let origin = <::Origin as From>::from( + s.origin.clone() + ).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()); + 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(); + if let &Some((period, count)) = &s.maybe_periodic { + if count > 1 { + s.maybe_periodic = Some((period, count - 1)); + } else { + s.maybe_periodic = None; + } + let next = now + period; + // If scheduled is named, place it's information in `Lookup` + if let Some(ref id) = s.maybe_id { + let next_index = Agenda::::decode_len(now + period).unwrap_or(0); + Lookup::::insert(id, (next, next_index as u32)); + } + Agenda::::append(next, Some(s)); + } else { + if let Some(ref id) = s.maybe_id { + Lookup::::remove(id); + } + } + Self::deposit_event(RawEvent::Dispatched( + (now, index), + maybe_id, + r.map(|_| ()).map_err(|e| e.error) + )); + total_weight = cumulative_weight; + None + } else { + Some(Some(s)) + } + }) + .for_each(|unused| { + let next = now + One::one(); + Agenda::::append(next, unused); + }); + + total_weight + } + } +} + +impl Module { + /// Migrate storage format from V1 to V2. + /// Return true if migration is performed. + pub fn migrate_v1_to_t2() -> bool { + if StorageVersion::get() == Releases::V1 { + StorageVersion::put(Releases::V2); + + Agenda::::translate::< + Vec::Call, T::BlockNumber>>>, _ + >(|_, agenda| Some( + agenda + .into_iter() + .map(|schedule| schedule.map(|schedule| ScheduledV2 { + maybe_id: schedule.maybe_id, + priority: schedule.priority, + call: schedule.call, + maybe_periodic: schedule.maybe_periodic, + origin: system::RawOrigin::Root.into(), + _phantom: Default::default(), + })) + .collect::>() + )); + + true + } else { + false + } + } + + /// Helper to migrate scheduler when the pallet origin type has changed. + pub fn migrate_origin + codec::Decode>() { + Agenda::::translate::< + Vec::Call, T::BlockNumber, OldOrigin, T::AccountId>>>, _ + >(|_, agenda| Some( + agenda + .into_iter() + .map(|schedule| schedule.map(|schedule| Scheduled { + maybe_id: schedule.maybe_id, + priority: schedule.priority, + call: schedule.call, + maybe_periodic: schedule.maybe_periodic, + origin: schedule.origin.into(), + _phantom: Default::default(), + })) + .collect::>() + )); + } + + fn resolve_time(when: DispatchTime) -> Result { + let now = frame_system::Pallet::::block_number(); + + let when = match when { + DispatchTime::At(x) => x, + // The current block has already completed it's scheduled tasks, so + // Schedule the task at lest one block after this current block. + DispatchTime::After(x) => now.saturating_add(x).saturating_add(One::one()) + }; + + if when <= now { + return Err(Error::::TargetBlockNumberInPast.into()) + } + + Ok(when) + } + + fn do_schedule( + when: DispatchTime, + maybe_periodic: Option>, + priority: schedule::Priority, + origin: T::PalletsOrigin, + call: ::Call + ) -> Result, DispatchError> { + let when = Self::resolve_time(when)?; + + // sanitize maybe_periodic + let maybe_periodic = maybe_periodic + .filter(|p| p.1 > 1 && !p.0.is_zero()) + // Remove one from the number of repetitions since we will schedule one now. + .map(|(p, c)| (p, c - 1)); + let s = Some(Scheduled { + maybe_id: None, priority, call, maybe_periodic, origin, _phantom: PhantomData::::default(), + }); + Agenda::::append(when, s); + let index = Agenda::::decode_len(when).unwrap_or(1) as u32 - 1; + if index > T::MaxScheduledPerBlock::get() { + log::warn!( + target: "runtime::scheduler", + "Warning: There are more items queued in the Scheduler than \ + expected from the runtime configuration. An update might be needed.", + ); + } + Self::deposit_event(RawEvent::Scheduled(when, index)); + + Ok((when, index)) + } + + fn do_cancel( + origin: Option, + (when, index): TaskAddress + ) -> Result<(), DispatchError> { + let scheduled = Agenda::::try_mutate( + when, + |agenda| { + agenda.get_mut(index as usize) + .map_or(Ok(None), |s| -> Result>, DispatchError> { + if let (Some(ref o), Some(ref s)) = (origin, s.borrow()) { + if *o != s.origin { + return Err(BadOrigin.into()); + } + }; + Ok(s.take()) + }) + }, + )?; + if let Some(s) = scheduled { + if let Some(id) = s.maybe_id { + Lookup::::remove(id); + } + Self::deposit_event(RawEvent::Canceled(when, index)); + Ok(()) + } else { + Err(Error::::NotFound)? + } + } + + fn do_reschedule( + (when, index): TaskAddress, + new_time: DispatchTime, + ) -> Result, DispatchError> { + let new_time = Self::resolve_time(new_time)?; + + if new_time == when { + return Err(Error::::RescheduleNoChange.into()); + } + + Agenda::::try_mutate(when, |agenda| -> DispatchResult { + let task = agenda.get_mut(index as usize).ok_or(Error::::NotFound)?; + let task = task.take().ok_or(Error::::NotFound)?; + Agenda::::append(new_time, Some(task)); + Ok(()) + })?; + + let new_index = Agenda::::decode_len(new_time).unwrap_or(1) as u32 - 1; + Self::deposit_event(RawEvent::Canceled(when, index)); + Self::deposit_event(RawEvent::Scheduled(new_time, new_index)); + + Ok((new_time, new_index)) + } + + fn do_schedule_named( + id: Vec, + when: DispatchTime, + maybe_periodic: Option>, + priority: schedule::Priority, + origin: T::PalletsOrigin, + call: ::Call, + ) -> Result, DispatchError> { + // ensure id it is unique + if Lookup::::contains_key(&id) { + return Err(Error::::FailedToSchedule)? + } + + let when = Self::resolve_time(when)?; + + // sanitize maybe_periodic + let maybe_periodic = maybe_periodic + .filter(|p| p.1 > 1 && !p.0.is_zero()) + // Remove one from the number of repetitions since we will schedule one now. + .map(|(p, c)| (p, c - 1)); + + let s = Scheduled { + maybe_id: Some(id.clone()), priority, call, maybe_periodic, origin, _phantom: Default::default() + }; + Agenda::::append(when, Some(s)); + let index = Agenda::::decode_len(when).unwrap_or(1) as u32 - 1; + if index > T::MaxScheduledPerBlock::get() { + log::warn!( + target: "runtime::scheduler", + "Warning: There are more items queued in the Scheduler than \ + expected from the runtime configuration. An update might be needed.", + ); + } + let address = (when, index); + Lookup::::insert(&id, &address); + Self::deposit_event(RawEvent::Scheduled(when, index)); + + Ok(address) + } + + fn do_cancel_named(origin: Option, id: Vec) -> DispatchResult { + Lookup::::try_mutate_exists(id, |lookup| -> DispatchResult { + if let Some((when, index)) = lookup.take() { + let i = index as usize; + Agenda::::try_mutate(when, |agenda| -> DispatchResult { + if let Some(s) = agenda.get_mut(i) { + if let (Some(ref o), Some(ref s)) = (origin, s.borrow()) { + if *o != s.origin { + return Err(BadOrigin.into()); + } + } + *s = None; + } + Ok(()) + })?; + Self::deposit_event(RawEvent::Canceled(when, index)); + Ok(()) + } else { + Err(Error::::NotFound)? + } + }) + } + + fn do_reschedule_named( + id: Vec, + new_time: DispatchTime, + ) -> Result, DispatchError> { + let new_time = Self::resolve_time(new_time)?; + + Lookup::::try_mutate_exists(id, |lookup| -> Result, DispatchError> { + let (when, index) = lookup.ok_or(Error::::NotFound)?; + + if new_time == when { + return Err(Error::::RescheduleNoChange.into()); + } + + Agenda::::try_mutate(when, |agenda| -> DispatchResult { + let task = agenda.get_mut(index as usize).ok_or(Error::::NotFound)?; + let task = task.take().ok_or(Error::::NotFound)?; + Agenda::::append(new_time, Some(task)); + + Ok(()) + })?; + + let new_index = Agenda::::decode_len(new_time).unwrap_or(1) as u32 - 1; + Self::deposit_event(RawEvent::Canceled(when, index)); + Self::deposit_event(RawEvent::Scheduled(new_time, new_index)); + + *lookup = Some((new_time, new_index)); + + Ok((new_time, new_index)) + }) + } +} + +impl schedule::Anon::Call, T::PalletsOrigin> for Module { + type Address = TaskAddress; + + fn schedule( + when: DispatchTime, + maybe_periodic: Option>, + priority: schedule::Priority, + origin: T::PalletsOrigin, + call: ::Call + ) -> Result { + Self::do_schedule(when, maybe_periodic, priority, origin, call) + } + + fn cancel((when, index): Self::Address) -> Result<(), ()> { + Self::do_cancel(None, (when, index)).map_err(|_| ()) + } + + fn reschedule( + address: Self::Address, + when: DispatchTime, + ) -> Result { + Self::do_reschedule(address, when) + } + + fn next_dispatch_time((when, index): Self::Address) -> Result { + Agenda::::get(when).get(index as usize).ok_or(()).map(|_| when) + } +} + +impl schedule::Named::Call, T::PalletsOrigin> for Module { + type Address = TaskAddress; + + fn schedule_named( + id: Vec, + when: DispatchTime, + maybe_periodic: Option>, + priority: schedule::Priority, + origin: T::PalletsOrigin, + call: ::Call, + ) -> Result { + Self::do_schedule_named(id, when, maybe_periodic, priority, origin, call).map_err(|_| ()) + } + + fn cancel_named(id: Vec) -> Result<(), ()> { + Self::do_cancel_named(None, id).map_err(|_| ()) + } + + fn reschedule_named( + id: Vec, + when: DispatchTime, + ) -> Result { + Self::do_reschedule_named(id, when) + } + + fn next_dispatch_time(id: Vec) -> Result { + Lookup::::get(id).and_then(|(when, index)| Agenda::::get(when).get(index as usize).map(|_| when)).ok_or(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use frame_support::{ + parameter_types, assert_ok, ord_parameter_types, + assert_noop, assert_err, Hashable, + traits::{OnInitialize, OnFinalize, Filter}, + weights::constants::RocksDbWeight, + }; + use sp_core::H256; + use sp_runtime::{ + Perbill, + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + }; + use frame_system::{EnsureOneOf, EnsureRoot, EnsureSignedBy}; + use substrate_test_utils::assert_eq_uvec; + use crate as scheduler; + + mod logger { + use super::*; + use std::cell::RefCell; + + thread_local! { + static LOG: RefCell> = RefCell::new(Vec::new()); + } + pub fn log() -> Vec<(OriginCaller, u32)> { + LOG.with(|log| log.borrow().clone()) + } + pub trait Config: system::Config { + type Event: From + Into<::Event>; + } + decl_event! { + pub enum Event { + Logged(u32, Weight), + } + } + decl_module! { + pub struct Module for enum Call + where + origin: ::Origin, + ::Origin: OriginTrait + { + fn deposit_event() = default; + + #[weight = *weight] + fn log(origin, i: u32, weight: Weight) { + Self::deposit_event(Event::Logged(i, weight)); + LOG.with(|log| { + log.borrow_mut().push((origin.caller().clone(), i)); + }) + } + + #[weight = *weight] + fn log_without_filter(origin, i: u32, weight: Weight) { + Self::deposit_event(Event::Logged(i, weight)); + LOG.with(|log| { + log.borrow_mut().push((origin.caller().clone(), i)); + }) + } + } + } + } + + type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; + type Block = frame_system::mocking::MockBlock; + + frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Logger: logger::{Pallet, Call, Event}, + Scheduler: scheduler::{Pallet, Call, Storage, Event}, + } + ); + + // Scheduler must dispatch with root and no filter, this tests base filter is indeed not used. + pub struct BaseFilter; + impl Filter for BaseFilter { + fn filter(call: &Call) -> bool { + !matches!(call, Call::Logger(logger::Call::log(_, _))) + } + } + + parameter_types! { + pub const BlockHashCount: u64 = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(2_000_000_000_000); + } + impl system::Config for Test { + type BaseCallFilter = BaseFilter; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = RocksDbWeight; + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + } + impl logger::Config for Test { + type Event = Event; + } + parameter_types! { + pub MaximumSchedulerWeight: Weight = Perbill::from_percent(80) * BlockWeights::get().max_block; + pub const MaxScheduledPerBlock: u32 = 10; + } + ord_parameter_types! { + pub const One: u64 = 1; + } + + impl Config for Test { + type Event = Event; + type Origin = Origin; + type PalletsOrigin = OriginCaller; + type Call = Call; + type MaximumWeight = MaximumSchedulerWeight; + type ScheduleOrigin = EnsureOneOf, EnsureSignedBy>; + type MaxScheduledPerBlock = MaxScheduledPerBlock; + type WeightInfo = (); + } + + pub fn new_test_ext() -> sp_io::TestExternalities { + let t = system::GenesisConfig::default().build_storage::().unwrap(); + t.into() + } + + fn run_to_block(n: u64) { + while System::block_number() < n { + Scheduler::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + Scheduler::on_initialize(System::block_number()); + } + } + + fn root() -> OriginCaller { + system::RawOrigin::Root.into() + } + + #[test] + fn basic_scheduling_works() { + new_test_ext().execute_with(|| { + let call = Call::Logger(logger::Call::log(42, 1000)); + assert!(!::BaseCallFilter::filter(&call)); + assert_ok!(Scheduler::do_schedule(DispatchTime::At(4), None, 127, root(), call)); + run_to_block(3); + assert!(logger::log().is_empty()); + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + }); + } + + #[test] + fn schedule_after_works() { + new_test_ext().execute_with(|| { + run_to_block(2); + let call = Call::Logger(logger::Call::log(42, 1000)); + assert!(!::BaseCallFilter::filter(&call)); + // This will schedule the call 3 blocks after the next block... so block 3 + 3 = 6 + assert_ok!(Scheduler::do_schedule(DispatchTime::After(3), None, 127, root(), call)); + run_to_block(5); + assert!(logger::log().is_empty()); + run_to_block(6); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + }); + } + + #[test] + fn schedule_after_zero_works() { + new_test_ext().execute_with(|| { + run_to_block(2); + let call = Call::Logger(logger::Call::log(42, 1000)); + assert!(!::BaseCallFilter::filter(&call)); + assert_ok!(Scheduler::do_schedule(DispatchTime::After(0), None, 127, root(), call)); + // Will trigger on the next block. + run_to_block(3); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + }); + } + + #[test] + fn periodic_scheduling_works() { + new_test_ext().execute_with(|| { + // at #4, every 3 blocks, 3 times. + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), Some((3, 3)), 127, root(), Call::Logger(logger::Call::log(42, 1000)) + )); + run_to_block(3); + assert!(logger::log().is_empty()); + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(6); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(7); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32)]); + run_to_block(9); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32)]); + run_to_block(10); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32), (root(), 42u32)]); + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32), (root(), 42u32)]); + }); + } + + #[test] + fn reschedule_works() { + new_test_ext().execute_with(|| { + let call = Call::Logger(logger::Call::log(42, 1000)); + assert!(!::BaseCallFilter::filter(&call)); + assert_eq!(Scheduler::do_schedule(DispatchTime::At(4), None, 127, root(), call).unwrap(), (4, 0)); + + run_to_block(3); + assert!(logger::log().is_empty()); + + assert_eq!(Scheduler::do_reschedule((4, 0), DispatchTime::At(6)).unwrap(), (6, 0)); + + assert_noop!(Scheduler::do_reschedule((6, 0), DispatchTime::At(6)), Error::::RescheduleNoChange); + + run_to_block(4); + assert!(logger::log().is_empty()); + + run_to_block(6); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + }); + } + + #[test] + fn reschedule_named_works() { + new_test_ext().execute_with(|| { + let call = Call::Logger(logger::Call::log(42, 1000)); + assert!(!::BaseCallFilter::filter(&call)); + assert_eq!(Scheduler::do_schedule_named( + 1u32.encode(), DispatchTime::At(4), None, 127, root(), call + ).unwrap(), (4, 0)); + + run_to_block(3); + assert!(logger::log().is_empty()); + + assert_eq!(Scheduler::do_reschedule_named(1u32.encode(), DispatchTime::At(6)).unwrap(), (6, 0)); + + assert_noop!(Scheduler::do_reschedule_named(1u32.encode(), DispatchTime::At(6)), Error::::RescheduleNoChange); + + run_to_block(4); + assert!(logger::log().is_empty()); + + run_to_block(6); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + }); + } + + #[test] + fn reschedule_named_perodic_works() { + new_test_ext().execute_with(|| { + let call = Call::Logger(logger::Call::log(42, 1000)); + assert!(!::BaseCallFilter::filter(&call)); + assert_eq!(Scheduler::do_schedule_named( + 1u32.encode(), DispatchTime::At(4), Some((3, 3)), 127, root(), call + ).unwrap(), (4, 0)); + + run_to_block(3); + assert!(logger::log().is_empty()); + + assert_eq!(Scheduler::do_reschedule_named(1u32.encode(), DispatchTime::At(5)).unwrap(), (5, 0)); + assert_eq!(Scheduler::do_reschedule_named(1u32.encode(), DispatchTime::At(6)).unwrap(), (6, 0)); + + run_to_block(5); + assert!(logger::log().is_empty()); + + run_to_block(6); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + + assert_eq!(Scheduler::do_reschedule_named(1u32.encode(), DispatchTime::At(10)).unwrap(), (10, 0)); + + run_to_block(9); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + + run_to_block(10); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32)]); + + run_to_block(13); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32), (root(), 42u32)]); + + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 42u32), (root(), 42u32)]); + }); + } + + #[test] + fn cancel_named_scheduling_works_with_normal_cancel() { + new_test_ext().execute_with(|| { + // at #4. + Scheduler::do_schedule_named( + 1u32.encode(), DispatchTime::At(4), None, 127, root(), Call::Logger(logger::Call::log(69, 1000)) + ).unwrap(); + let i = Scheduler::do_schedule( + DispatchTime::At(4), None, 127, root(), Call::Logger(logger::Call::log(42, 1000)) + ).unwrap(); + run_to_block(3); + assert!(logger::log().is_empty()); + assert_ok!(Scheduler::do_cancel_named(None, 1u32.encode())); + assert_ok!(Scheduler::do_cancel(None, i)); + run_to_block(100); + assert!(logger::log().is_empty()); + }); + } + + #[test] + fn cancel_named_periodic_scheduling_works() { + new_test_ext().execute_with(|| { + // at #4, every 3 blocks, 3 times. + Scheduler::do_schedule_named( + 1u32.encode(), + DispatchTime::At(4), + Some((3, 3)), + 127, + root(), + Call::Logger(logger::Call::log(42, 1000)) + ).unwrap(); + // same id results in error. + assert!(Scheduler::do_schedule_named( + 1u32.encode(), + DispatchTime::At(4), + None, + 127, + root(), + Call::Logger(logger::Call::log(69, 1000)) + ).is_err()); + // different id is ok. + Scheduler::do_schedule_named( + 2u32.encode(), DispatchTime::At(8), None, 127, root(), Call::Logger(logger::Call::log(69, 1000)) + ).unwrap(); + run_to_block(3); + assert!(logger::log().is_empty()); + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(6); + assert_ok!(Scheduler::do_cancel_named(None, 1u32.encode())); + run_to_block(100); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 69u32)]); + }); + } + + #[test] + fn scheduler_respects_weight_limits() { + new_test_ext().execute_with(|| { + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), + Call::Logger(logger::Call::log(42, MaximumSchedulerWeight::get() / 2)) + )); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), Call::Logger(logger::Call::log(69, MaximumSchedulerWeight::get() / 2)) + )); + // 69 and 42 do not fit together + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 42u32)]); + run_to_block(5); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 69u32)]); + }); + } + + #[test] + fn scheduler_respects_hard_deadlines_more() { + new_test_ext().execute_with(|| { + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 0, + root(), + Call::Logger(logger::Call::log(42, MaximumSchedulerWeight::get() / 2)) + )); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 0, + root(), + Call::Logger(logger::Call::log(69, MaximumSchedulerWeight::get() / 2)) + )); + // With base weights, 69 and 42 should not fit together, but do because of hard deadlines + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 42u32), (root(), 69u32)]); + }); + } + + #[test] + fn scheduler_respects_priority_ordering() { + new_test_ext().execute_with(|| { + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 1, + root(), + Call::Logger(logger::Call::log(42, MaximumSchedulerWeight::get() / 2)) + )); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 0, + root(), + Call::Logger(logger::Call::log(69, MaximumSchedulerWeight::get() / 2)) + )); + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 69u32), (root(), 42u32)]); + }); + } + + #[test] + fn scheduler_respects_priority_ordering_with_soft_deadlines() { + new_test_ext().execute_with(|| { + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 255, + root(), Call::Logger(logger::Call::log(42, MaximumSchedulerWeight::get() / 3)) + )); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 127, + root(), Call::Logger(logger::Call::log(69, MaximumSchedulerWeight::get() / 2)) + )); + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(4), + None, + 126, + root(), Call::Logger(logger::Call::log(2600, MaximumSchedulerWeight::get() / 2)) + )); + + // 2600 does not fit with 69 or 42, but has higher priority, so will go through + run_to_block(4); + assert_eq!(logger::log(), vec![(root(), 2600u32)]); + // 69 and 42 fit together + run_to_block(5); + assert_eq!(logger::log(), vec![(root(), 2600u32), (root(), 69u32), (root(), 42u32)]); + }); + } + + #[test] + fn on_initialize_weight_is_correct() { + new_test_ext().execute_with(|| { + let base_weight: Weight = ::DbWeight::get().reads_writes(1, 2); + let base_multiplier = 0; + let named_multiplier = ::DbWeight::get().writes(1); + let periodic_multiplier = ::DbWeight::get().reads_writes(1, 1); + + // Named + assert_ok!( + Scheduler::do_schedule_named( + 1u32.encode(), DispatchTime::At(1), None, 255, root(), + Call::Logger(logger::Call::log(3, MaximumSchedulerWeight::get() / 3)) + ) + ); + // Anon Periodic + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(1), + Some((1000, 3)), + 128, + root(), + Call::Logger(logger::Call::log(42, MaximumSchedulerWeight::get() / 3)) + )); + // Anon + assert_ok!(Scheduler::do_schedule( + DispatchTime::At(1), + None, + 127, + root(), + Call::Logger(logger::Call::log(69, MaximumSchedulerWeight::get() / 2)) + )); + // Named Periodic + assert_ok!(Scheduler::do_schedule_named( + 2u32.encode(), DispatchTime::At(1), Some((1000, 3)), 126, root(), + Call::Logger(logger::Call::log(2600, MaximumSchedulerWeight::get() / 2))) + ); + + // Will include the named periodic only + let actual_weight = Scheduler::on_initialize(1); + let call_weight = MaximumSchedulerWeight::get() / 2; + assert_eq!( + actual_weight, call_weight + base_weight + base_multiplier + named_multiplier + periodic_multiplier + ); + assert_eq!(logger::log(), vec![(root(), 2600u32)]); + + // Will include anon and anon periodic + let actual_weight = Scheduler::on_initialize(2); + let call_weight = MaximumSchedulerWeight::get() / 2 + MaximumSchedulerWeight::get() / 3; + assert_eq!(actual_weight, call_weight + base_weight + base_multiplier * 2 + periodic_multiplier); + assert_eq!(logger::log(), vec![(root(), 2600u32), (root(), 69u32), (root(), 42u32)]); + + // Will include named only + let actual_weight = Scheduler::on_initialize(3); + let call_weight = MaximumSchedulerWeight::get() / 3; + assert_eq!(actual_weight, call_weight + base_weight + base_multiplier + named_multiplier); + assert_eq!(logger::log(), vec![(root(), 2600u32), (root(), 69u32), (root(), 42u32), (root(), 3u32)]); + + // Will contain none + let actual_weight = Scheduler::on_initialize(4); + assert_eq!(actual_weight, 0); + }); + } + + #[test] + fn root_calls_works() { + new_test_ext().execute_with(|| { + let call = Box::new(Call::Logger(logger::Call::log(69, 1000))); + let call2 = Box::new(Call::Logger(logger::Call::log(42, 1000))); + assert_ok!(Scheduler::schedule_named(Origin::root(), 1u32.encode(), 4, None, 127, call)); + assert_ok!(Scheduler::schedule(Origin::root(), 4, None, 127, call2)); + run_to_block(3); + // Scheduled calls are in the agenda. + assert_eq!(Agenda::::get(4).len(), 2); + assert!(logger::log().is_empty()); + assert_ok!(Scheduler::cancel_named(Origin::root(), 1u32.encode())); + assert_ok!(Scheduler::cancel(Origin::root(), 4, 1)); + // Scheduled calls are made NONE, so should not effect state + run_to_block(100); + assert!(logger::log().is_empty()); + }); + } + + #[test] + fn fails_to_schedule_task_in_the_past() { + new_test_ext().execute_with(|| { + run_to_block(3); + + let call = Box::new(Call::Logger(logger::Call::log(69, 1000))); + let call2 = Box::new(Call::Logger(logger::Call::log(42, 1000))); + + assert_err!( + Scheduler::schedule_named(Origin::root(), 1u32.encode(), 2, None, 127, call), + Error::::TargetBlockNumberInPast, + ); + + assert_err!( + Scheduler::schedule(Origin::root(), 2, None, 127, call2.clone()), + Error::::TargetBlockNumberInPast, + ); + + assert_err!( + Scheduler::schedule(Origin::root(), 3, None, 127, call2), + Error::::TargetBlockNumberInPast, + ); + }); + } + + #[test] + fn should_use_orign() { + new_test_ext().execute_with(|| { + let call = Box::new(Call::Logger(logger::Call::log(69, 1000))); + let call2 = Box::new(Call::Logger(logger::Call::log(42, 1000))); + assert_ok!( + Scheduler::schedule_named(system::RawOrigin::Signed(1).into(), 1u32.encode(), 4, None, 127, call) + ); + assert_ok!(Scheduler::schedule(system::RawOrigin::Signed(1).into(), 4, None, 127, call2)); + run_to_block(3); + // Scheduled calls are in the agenda. + assert_eq!(Agenda::::get(4).len(), 2); + assert!(logger::log().is_empty()); + assert_ok!(Scheduler::cancel_named(system::RawOrigin::Signed(1).into(), 1u32.encode())); + assert_ok!(Scheduler::cancel(system::RawOrigin::Signed(1).into(), 4, 1)); + // Scheduled calls are made NONE, so should not effect state + run_to_block(100); + assert!(logger::log().is_empty()); + }); + } + + #[test] + fn should_check_orign() { + new_test_ext().execute_with(|| { + let call = Box::new(Call::Logger(logger::Call::log(69, 1000))); + let call2 = Box::new(Call::Logger(logger::Call::log(42, 1000))); + assert_noop!( + Scheduler::schedule_named(system::RawOrigin::Signed(2).into(), 1u32.encode(), 4, None, 127, call), + BadOrigin + ); + assert_noop!(Scheduler::schedule(system::RawOrigin::Signed(2).into(), 4, None, 127, call2), BadOrigin); + }); + } + + #[test] + fn should_check_orign_for_cancel() { + new_test_ext().execute_with(|| { + let call = Box::new(Call::Logger(logger::Call::log_without_filter(69, 1000))); + let call2 = Box::new(Call::Logger(logger::Call::log_without_filter(42, 1000))); + assert_ok!( + Scheduler::schedule_named(system::RawOrigin::Signed(1).into(), 1u32.encode(), 4, None, 127, call) + ); + assert_ok!(Scheduler::schedule(system::RawOrigin::Signed(1).into(), 4, None, 127, call2)); + run_to_block(3); + // Scheduled calls are in the agenda. + assert_eq!(Agenda::::get(4).len(), 2); + assert!(logger::log().is_empty()); + assert_noop!(Scheduler::cancel_named(system::RawOrigin::Signed(2).into(), 1u32.encode()), BadOrigin); + assert_noop!(Scheduler::cancel(system::RawOrigin::Signed(2).into(), 4, 1), BadOrigin); + assert_noop!(Scheduler::cancel_named(system::RawOrigin::Root.into(), 1u32.encode()), BadOrigin); + assert_noop!(Scheduler::cancel(system::RawOrigin::Root.into(), 4, 1), BadOrigin); + run_to_block(5); + assert_eq!( + logger::log(), + vec![(system::RawOrigin::Signed(1).into(), 69u32), (system::RawOrigin::Signed(1).into(), 42u32)] + ); + }); + } + + #[test] + fn migration_to_v2_works() { + new_test_ext().execute_with(|| { + for i in 0..3u64 { + let k = i.twox_64_concat(); + let old = vec![ + Some(ScheduledV1 { + maybe_id: None, + priority: i as u8 + 10, + call: Call::Logger(logger::Call::log(96, 100)), + maybe_periodic: None, + }), + None, + Some(ScheduledV1 { + maybe_id: Some(b"test".to_vec()), + priority: 123, + call: Call::Logger(logger::Call::log(69, 1000)), + maybe_periodic: Some((456u64, 10)), + }), + ]; + frame_support::migration::put_storage_value( + b"Scheduler", + b"Agenda", + &k, + old, + ); + } + + assert_eq!(StorageVersion::get(), Releases::V1); + + assert!(Scheduler::migrate_v1_to_t2()); + + assert_eq_uvec!(Agenda::::iter().collect::>(), vec![ + ( + 0, + vec![ + Some(ScheduledV2 { + maybe_id: None, + priority: 10, + call: Call::Logger(logger::Call::log(96, 100)), + maybe_periodic: None, + origin: root(), + _phantom: PhantomData::::default(), + }), + None, + Some(ScheduledV2 { + maybe_id: Some(b"test".to_vec()), + priority: 123, + call: Call::Logger(logger::Call::log(69, 1000)), + maybe_periodic: Some((456u64, 10)), + origin: root(), + _phantom: PhantomData::::default(), + }), + ]), + ( + 1, + vec![ + Some(ScheduledV2 { + maybe_id: None, + priority: 11, + call: Call::Logger(logger::Call::log(96, 100)), + maybe_periodic: None, + origin: root(), + _phantom: PhantomData::::default(), + }), + None, + Some(ScheduledV2 { + maybe_id: Some(b"test".to_vec()), + priority: 123, + call: Call::Logger(logger::Call::log(69, 1000)), + maybe_periodic: Some((456u64, 10)), + origin: root(), + _phantom: PhantomData::::default(), + }), + ] + ), + ( + 2, + vec![ + Some(ScheduledV2 { + maybe_id: None, + priority: 12, + call: Call::Logger(logger::Call::log(96, 100)), + maybe_periodic: None, + origin: root(), + _phantom: PhantomData::::default(), + }), + None, + Some(ScheduledV2 { + maybe_id: Some(b"test".to_vec()), + priority: 123, + call: Call::Logger(logger::Call::log(69, 1000)), + maybe_periodic: Some((456u64, 10)), + origin: root(), + _phantom: PhantomData::::default(), + }), + ] + ) + ]); + + assert_eq!(StorageVersion::get(), Releases::V2); + }); + } + + #[test] + fn test_migrate_origin() { + new_test_ext().execute_with(|| { + for i in 0..3u64 { + let k = i.twox_64_concat(); + let old: Vec>> = vec![ + Some(Scheduled { + maybe_id: None, + priority: i as u8 + 10, + call: Call::Logger(logger::Call::log(96, 100)), + origin: 3u32, + maybe_periodic: None, + _phantom: Default::default(), + }), + None, + Some(Scheduled { + maybe_id: Some(b"test".to_vec()), + priority: 123, + origin: 2u32, + call: Call::Logger(logger::Call::log(69, 1000)), + maybe_periodic: Some((456u64, 10)), + _phantom: Default::default(), + }), + ]; + frame_support::migration::put_storage_value( + b"Scheduler", + b"Agenda", + &k, + old, + ); + } + + impl Into for u32 { + fn into(self) -> OriginCaller { + match self { + 3u32 => system::RawOrigin::Root.into(), + 2u32 => system::RawOrigin::None.into(), + _ => unreachable!("test make no use of it"), + } + } + } + + Scheduler::migrate_origin::(); + + assert_eq_uvec!(Agenda::::iter().collect::>(), vec![ + ( + 0, + vec![ + Some(ScheduledV2::<_, _, OriginCaller, u64> { + maybe_id: None, + priority: 10, + call: Call::Logger(logger::Call::log(96, 100)), + maybe_periodic: None, + origin: system::RawOrigin::Root.into(), + _phantom: PhantomData::::default(), + }), + None, + Some(ScheduledV2 { + maybe_id: Some(b"test".to_vec()), + priority: 123, + call: Call::Logger(logger::Call::log(69, 1000)), + maybe_periodic: Some((456u64, 10)), + origin: system::RawOrigin::None.into(), + _phantom: PhantomData::::default(), + }), + ]), + ( + 1, + vec![ + Some(ScheduledV2 { + maybe_id: None, + priority: 11, + call: Call::Logger(logger::Call::log(96, 100)), + maybe_periodic: None, + origin: system::RawOrigin::Root.into(), + _phantom: PhantomData::::default(), + }), + None, + Some(ScheduledV2 { + maybe_id: Some(b"test".to_vec()), + priority: 123, + call: Call::Logger(logger::Call::log(69, 1000)), + maybe_periodic: Some((456u64, 10)), + origin: system::RawOrigin::None.into(), + _phantom: PhantomData::::default(), + }), + ] + ), + ( + 2, + vec![ + Some(ScheduledV2 { + maybe_id: None, + priority: 12, + call: Call::Logger(logger::Call::log(96, 100)), + maybe_periodic: None, + origin: system::RawOrigin::Root.into(), + _phantom: PhantomData::::default(), + }), + None, + Some(ScheduledV2 { + maybe_id: Some(b"test".to_vec()), + priority: 123, + call: Call::Logger(logger::Call::log(69, 1000)), + maybe_periodic: Some((456u64, 10)), + origin: system::RawOrigin::None.into(), + _phantom: PhantomData::::default(), + }), + ] + ) + ]); + }); + } +} --- /dev/null +++ b/pallets/scheduler/src/weights.rs @@ -0,0 +1,118 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Weights for pallet_scheduler +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 2.0.0 +//! DATE: 2020-10-27, STEPS: `[50, ]`, REPEAT: 20, LOW RANGE: [], HIGH RANGE: [] +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128 + +// Executed Command: +// target/release/substrate +// benchmark +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_scheduler +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/scheduler/src/weights.rs +// --template=./.maintain/frame-weight-template.hbs + + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_scheduler. +pub trait WeightInfo { + fn schedule(s: u32, ) -> Weight; + fn cancel(s: u32, ) -> Weight; + fn schedule_named(s: u32, ) -> Weight; + fn cancel_named(s: u32, ) -> Weight; + +} + +/// Weights for pallet_scheduler using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + fn schedule(s: u32, ) -> Weight { + (35_029_000 as Weight) + .saturating_add((77_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + + } + fn cancel(s: u32, ) -> Weight { + (31_419_000 as Weight) + .saturating_add((4_015_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + + } + fn schedule_named(s: u32, ) -> Weight { + (44_752_000 as Weight) + .saturating_add((123_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + + } + fn cancel_named(s: u32, ) -> Weight { + (35_712_000 as Weight) + .saturating_add((4_008_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + + } + +} + +// For backwards compatibility and tests +impl WeightInfo for () { + fn schedule(s: u32, ) -> Weight { + (35_029_000 as Weight) + .saturating_add((77_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + + } + fn cancel(s: u32, ) -> Weight { + (31_419_000 as Weight) + .saturating_add((4_015_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + + } + fn schedule_named(s: u32, ) -> Weight { + (44_752_000 as Weight) + .saturating_add((123_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + + } + fn cancel_named(s: u32, ) -> Weight { + (35_712_000 as Weight) + .saturating_add((4_008_000 as Weight).saturating_mul(s as Weight)) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) + + } + +} --- /dev/null +++ b/primitives/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "nft-data-structs" +authors = ['Substrate DevHub '] +description = "Nft data structs definitions" +edition = "2018" +license = 'GPL-3.0' +homepage = "https://substrate.dev" +repository = 'https://github.com/clover-network/clover' +version = '0.9.0' + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false } +serde = { version = "1.0.101", optional = true } +frame-support = { default-features = false, version = '3.0.0' } +frame-system = { default-features = false, version = '3.0.0' } +pallet-contracts = { default-features = false, version = '3.0.0' } +sp-core = { version = "3.0.0", default-features = false } +sp-runtime = { version = "3.0.0", default-features = false } + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "frame-system/std", + "frame-support/std", + "sp-runtime/std", + "sp-core/std", + "pallet-contracts/std", +] \ No newline at end of file --- /dev/null +++ b/primitives/src/lib.rs @@ -0,0 +1,306 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode}; + +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; + +use frame_system; +use core::ops::{Deref, DerefMut}; +use sp_runtime::sp_std::prelude::Vec; +pub use frame_support::{ + construct_runtime, decl_event, decl_module, decl_storage, decl_error, + dispatch::DispatchResult, + ensure, fail, parameter_types, + traits::{ + Currency, ExistenceRequirement, Get, Imbalance, KeyOwnerProofSystem, OnUnbalanced, + Randomness, IsSubType, WithdrawReasons, + }, + weights::{ + constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND}, + DispatchInfo, GetDispatchInfo, IdentityFee, Pays, PostDispatchInfo, Weight, + WeightToFeePolynomial, DispatchClass, + }, + StorageValue, + transactional, +}; + +pub const MAX_DECIMAL_POINTS: DecimalPoints = 30; +pub const MAX_REFUNGIBLE_PIECES: u128 = 1_000_000_000_000_000_000_000; +pub const MAX_SPONSOR_TIMEOUT: u32 = 10_368_000; +pub const MAX_TOKEN_OWNERSHIP: u32 = 10_000_000; + +pub type CollectionId = u32; +pub type TokenId = u32; +pub type DecimalPoints = u8; + +#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum CollectionMode { + Invalid, + NFT, + // decimal points + Fungible(DecimalPoints), + ReFungible, +} + +impl Default for CollectionMode { + fn default() -> Self { + Self::Invalid + } +} + +impl Into for CollectionMode { + fn into(self) -> u8 { + match self { + CollectionMode::Invalid => 0, + CollectionMode::NFT => 1, + CollectionMode::Fungible(_) => 2, + CollectionMode::ReFungible => 3, + } + } +} + +pub trait SponsoringResolve { + fn resolve( + who: &AccountId, + call: &Call) -> Option; +} + +#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum AccessMode { + Normal, + WhiteList, +} +impl Default for AccessMode { + fn default() -> Self { + Self::Normal + } +} + +#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum SchemaVersion { + ImageURL, + Unique, +} +impl Default for SchemaVersion { + fn default() -> Self { + Self::ImageURL + } +} + +#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct Ownership { + pub owner: AccountId, + pub fraction: u128, +} + +#[derive(Encode, Decode, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum SponsorshipState { + /// The fees are applied to the transaction sender + Disabled, + Unconfirmed(AccountId), + /// Transactions are sponsored by specified account + Confirmed(AccountId), +} + +impl SponsorshipState { + pub fn sponsor(&self) -> Option<&AccountId> { + match self { + Self::Confirmed(sponsor) => Some(sponsor), + _ => None, + } + } + + pub fn pending_sponsor(&self) -> Option<&AccountId> { + match self { + Self::Unconfirmed(sponsor) | Self::Confirmed(sponsor) => Some(sponsor), + _ => None, + } + } + + pub fn confirmed(&self) -> bool { + matches!(self, Self::Confirmed(_)) + } +} + +impl Default for SponsorshipState { + fn default() -> Self { + Self::Disabled + } +} + +#[derive(Encode, Decode, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct Collection { + pub owner: T::AccountId, + pub mode: CollectionMode, + pub access: AccessMode, + pub decimal_points: DecimalPoints, + pub name: Vec, // 64 include null escape char + pub description: Vec, // 256 include null escape char + pub token_prefix: Vec, // 16 include null escape char + pub mint_mode: bool, + pub offchain_schema: Vec, + pub schema_version: SchemaVersion, + pub sponsorship: SponsorshipState, + pub limits: CollectionLimits, // Collection private restrictions + pub variable_on_chain_schema: Vec, // + pub const_on_chain_schema: Vec, // +} + +pub struct CollectionHandle { + pub id: CollectionId, + pub collection: Collection, +} + +impl Deref for CollectionHandle { + type Target = Collection; + + fn deref(&self) -> &Self::Target { + &self.collection + } +} + +impl DerefMut for CollectionHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.collection + } +} + +#[derive(Encode, Decode, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct NftItemType { + pub owner: AccountId, + pub const_data: Vec, + pub variable_data: Vec, +} + +#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct FungibleItemType { + pub value: u128, +} + +#[derive(Encode, Decode, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct ReFungibleItemType { + pub owner: Vec>, + pub const_data: Vec, + pub variable_data: Vec, +} + + +#[derive(Encode, Decode, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct CollectionLimits { + pub account_token_ownership_limit: u32, + pub sponsored_data_size: u32, + /// None - setVariableMetadata is not sponsored + /// Some(v) - setVariableMetadata is sponsored + /// if there is v block between txs + pub sponsored_data_rate_limit: Option, + pub token_limit: u32, + + // Timeouts for item types in passed blocks + pub sponsor_transfer_timeout: u32, + pub owner_can_transfer: bool, + pub owner_can_destroy: bool, +} + +impl Default for CollectionLimits { + fn default() -> Self { + Self { + account_token_ownership_limit: 10_000_000, + token_limit: u32::max_value(), + sponsored_data_size: u32::MAX, + sponsored_data_rate_limit: None, + sponsor_transfer_timeout: 14400, + owner_can_transfer: true, + owner_can_destroy: true + } + } +} + +#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct ChainLimits { + pub collection_numbers_limit: u32, + pub account_token_ownership_limit: u32, + pub collections_admins_limit: u64, + pub custom_data_limit: u32, + + // Timeouts for item types in passed blocks + pub nft_sponsor_transfer_timeout: u32, + pub fungible_sponsor_transfer_timeout: u32, + pub refungible_sponsor_transfer_timeout: u32, + + // Schema limits + pub offchain_schema_limit: u32, + pub variable_on_chain_schema_limit: u32, + pub const_on_chain_schema_limit: u32, +} + + +#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct CreateNftData { + pub const_data: Vec, + pub variable_data: Vec, +} + +#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct CreateFungibleData { + pub value: u128, +} + +#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct CreateReFungibleData { + pub const_data: Vec, + pub variable_data: Vec, + pub pieces: u128, +} + +#[derive(Encode, Decode, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum CreateItemData { + NFT(CreateNftData), + Fungible(CreateFungibleData), + ReFungible(CreateReFungibleData), +} + +impl CreateItemData { + pub fn len(&self) -> usize { + let len = match self { + CreateItemData::NFT(data) => data.variable_data.len() + data.const_data.len(), + CreateItemData::ReFungible(data) => data.variable_data.len() + data.const_data.len(), + _ => 0 + }; + + return len; + } +} + +impl From for CreateItemData { + fn from(item: CreateNftData) -> Self { + CreateItemData::NFT(item) + } +} + +impl From for CreateItemData { + fn from(item: CreateReFungibleData) -> Self { + CreateItemData::ReFungible(item) + } +} + +impl From for CreateItemData { + fn from(item: CreateFungibleData) -> Self { + CreateItemData::Fungible(item) + } +} \ No newline at end of file --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -26,7 +26,11 @@ # local dependencies pallet-nft = { path = '../pallets/nft', default-features = false, version = '3.0.0' } +nft-data-structs = { path = '../primitives', default-features = false } pallet-inflation = { path = '../pallets/inflation', default-features = false, version = '3.0.0' } +pallet-scheduler = { path = '../pallets/scheduler', default-features = false, version = '3.0.0' } +pallet-nft-transaction-payment = { path = '../pallets/nft-transaction-payment', default-features = false, version = '3.0.0' } +pallet-nft-charge-transaction = { path = '../pallets/nft-charge-transaction', default-features = false, version = '3.0.0' } # Substrate dependencies frame-benchmarking = { default-features = false, optional = true, version = '3.0.0' } @@ -96,9 +100,12 @@ 'pallet-transaction-payment-rpc-runtime-api/std', 'pallet-treasury/std', 'pallet-vesting/std', - 'pallet-inflation/std', 'pallet-nft/std', + 'pallet-scheduler/std', + 'pallet-nft-charge-transaction/std', + 'pallet-nft-transaction-payment/std', + 'nft-data-structs/std', 'sp-api/std', 'sp-block-builder/std', 'sp-consensus-aura/std', --- a/runtime/src/chain_extension.rs +++ b/runtime/src/chain_extension.rs @@ -17,7 +17,8 @@ extern crate pallet_nft; pub use pallet_nft::*; -use crate::Runtime; +use nft_data_structs::*; +// use crate::Runtime; use sp_runtime::AccountId32; use crate::Vec; @@ -41,7 +42,7 @@ /// The chain Extension of NFT pallet pub struct NFTExtension; -impl ChainExtension for NFTExtension { +impl ChainExtension for NFTExtension { fn call(func_id: u32, env: Environment) -> Result where E: Ext, --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -24,7 +24,7 @@ create_runtime_str, generic, impl_opaque_keys, traits::{ Convert, ConvertInto, BlakeTwo256, Block as BlockT, IdentifyAccount, - IdentityLookup, NumberFor, Verify, AccountIdConversion, + IdentityLookup, NumberFor, Verify, AccountIdConversion, }, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, MultiSignature, @@ -51,17 +51,28 @@ WeightToFeePolynomial, WeightToFeeCoefficient, WeightToFeeCoefficients }, }; +use pallet_nft_transaction_payment::*; +use pallet_nft_charge_transaction::*; +use nft_data_structs::*; use pallet_contracts::weights::WeightInfo; // #[cfg(any(feature = "std", test))] use frame_system::{ self as system, - EnsureRoot, + EnsureRoot, EnsureSigned, limits::{BlockWeights, BlockLength}, }; use sp_std::{prelude::*, marker::PhantomData}; use sp_arithmetic::{traits::{BaseArithmetic, Unsigned}}; use smallvec::smallvec; +use sp_runtime::{ + traits::{ + Dispatchable, + }, +}; +use pallet_contracts::chain_extension::UncheckedFrom; + + pub use pallet_timestamp::Call as TimestampCall; mod chain_extension; @@ -173,45 +184,8 @@ runtime_version: VERSION, can_author_with: Default::default(), } -} - -/// Provides a membership set with only the configured aura users -pub struct ValiudatorsOnly(PhantomData); -impl frame_support::traits::Contains for ValiudatorsOnly { - fn contains(t: &AccountId) -> bool { - let arr: [u8; 32] = *t.as_ref(); - let raw_key: Vec = Vec::from(arr); - - match pallet_aura::Module::::authorities().iter().find(|auth| auth.to_raw_vec() == raw_key) { - Some(_) => true, - None => false, - } - } - fn sorted_members() -> Vec { - let mut members: Vec = Vec::new(); - for auth in pallet_aura::Module::::authorities() { - let mut arr: [u8; 32] = Default::default(); - let bor_arr = auth.clone().to_raw_vec(); - let slice = bor_arr.as_slice(); - arr.copy_from_slice(slice); - members.push(AccountId::from(arr)); - } - members - } - fn count() -> usize { - pallet_aura::Module::::authorities().len() - } } -impl frame_support::traits::ContainsLengthBound for ValiudatorsOnly { - fn min_len() -> usize { - 1 - } - fn max_len() -> usize { - 100 - } -} - type NegativeImbalance = >::NegativeImbalance; pub struct DealWithFees; @@ -229,21 +203,6 @@ } } } - -// impl OnUnbalanced for DealWithFees { -// fn on_unbalanceds(mut fees_then_tips: impl Iterator) { -// if let Some(fees) = fees_then_tips.next() { -// // for fees, 100% to treasury -// let mut split = fees.ration(100, 0); -// if let Some(tips) = fees_then_tips.next() { -// // for tips, if any, 100% to treasury -// tips.ration_merge_into(100, 0, &mut split); -// } -// Treasury::on_unbalanced(split.0); -// // Author::on_unbalanced(split.1); -// } -// } -// } /// We assume that ~10% of the block weight is consumed by `on_initalize` handlers. /// This is used to limit the maximal weight of a single extrinsic. @@ -549,6 +508,46 @@ type InflationBlockInterval = InflationBlockInterval; } + +parameter_types! { + pub MaximumSchedulerWeight: Weight = Perbill::from_percent(50) * + RuntimeBlockWeights::get().max_block; + pub const MaxScheduledPerBlock: u32 = 50; +} + +pub struct Sponsoring; +impl SponsoringResolve for Sponsoring { + + fn resolve(who: &AccountId, call: &Call) -> Option + where + Call: Dispatchable, + Call: IsSubType>, + Call: IsSubType>, + AccountId: AsRef<[u8]>, + AccountId: UncheckedFrom + { + pallet_nft_transaction_payment::Module::::withdraw_type(who, call) + } +} + +impl pallet_scheduler::Config for Runtime { + type Event = Event; + type Origin = Origin; + type PalletsOrigin = OriginCaller; + type Call = Call; + type MaximumWeight = MaximumSchedulerWeight; + type ScheduleOrigin = EnsureSigned; + type MaxScheduledPerBlock = MaxScheduledPerBlock; + type Sponsoring = Sponsoring; + type WeightInfo = (); +} + +impl pallet_nft_transaction_payment::Config for Runtime { +} + +impl pallet_nft_charge_transaction::Config for Runtime { +} + construct_runtime!( pub enum Runtime where Block = Block, @@ -564,10 +563,13 @@ Balances: pallet_balances::{Module, Call, Storage, Config, Event}, TransactionPayment: pallet_transaction_payment::{Module, Storage}, Sudo: pallet_sudo::{Module, Call, Config, Storage, Event}, - Inflation: pallet_inflation::{Module, Call, Storage}, + Inflation: pallet_inflation::{Module, Call, Storage}, + Scheduler: pallet_scheduler::{Module, Call, Storage, Event}, Nft: pallet_nft::{Module, Call, Config, Storage, Event}, Treasury: pallet_treasury::{Module, Call, Storage, Config, Event}, - Vesting: pallet_vesting::{Module, Call, Config, Storage, Event}, + Vesting: pallet_vesting::{Module, Call, Config, Storage, Event}, + NftPayment: pallet_nft_transaction_payment::{Module, Call, Storage}, + Charging: pallet_nft_charge_transaction::{Module, Call, Storage }, } ); @@ -589,7 +591,7 @@ system::CheckEra, system::CheckNonce, system::CheckWeight, - pallet_nft::ChargeTransactionPayment, + pallet_nft_charge_transaction::ChargeTransactionPayment, ); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = generic::UncheckedExtrinsic; -- gitstuff