From 3bff6ee7498b190412dab2aadb4d823ddb80112b Mon Sep 17 00:00:00 2001 From: Shaopeng Wang <spxwang@gmail.com> Date: Tue, 19 Jan 2021 17:24:34 +1300 Subject: [PATCH] Migrate oracle & nft pallet (#357) * Migrate oracle module. * Migrate NFT module. * Impl genesis config default under std. --- nft/src/lib.rs | 424 ++++++++++++++------------- oracle/src/default_combine_data.rs | 8 +- oracle/src/lib.rs | 446 ++++++++++++++++------------- oracle/src/mock.rs | 5 +- oracle/src/tests.rs | 8 +- utilities/src/ordered_set.rs | 2 +- 6 files changed, 483 insertions(+), 410 deletions(-) diff --git a/nft/src/lib.rs b/nft/src/lib.rs index 3b43b09..e0e1cb9 100644 --- a/nft/src/lib.rs +++ b/nft/src/lib.rs @@ -19,56 +19,81 @@ //! - `destroy_class` - Destroy NFT(non fungible token) class #![cfg_attr(not(feature = "std"), no_std)] - -use codec::{Decode, Encode}; -use frame_support::{decl_error, decl_module, decl_storage, ensure, Parameter}; -use sp_runtime::{ - traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, Member, One, Zero}, - DispatchError, DispatchResult, RuntimeDebug, -}; -use sp_std::vec::Vec; +#![allow(clippy::unused_unit)] mod mock; mod tests; -/// Class info -#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] -pub struct ClassInfo<TokenId, AccountId, Data> { - /// Class metadata - pub metadata: Vec<u8>, - /// Total issuance for the class - pub total_issuance: TokenId, - /// Class owner - pub owner: AccountId, - /// Class Properties - pub data: Data, -} +pub use module::*; + +#[frame_support::pallet] +pub mod module { + use codec::{Decode, Encode}; + use frame_support::{ensure, pallet_prelude::*, Parameter}; + use sp_runtime::{ + traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, MaybeSerializeDeserialize, Member, One, Zero}, + DispatchError, DispatchResult, RuntimeDebug, + }; + use sp_std::vec::Vec; + + /// Class info + #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] + pub struct ClassInfo<TokenId, AccountId, Data> { + /// Class metadata + pub metadata: Vec<u8>, + /// Total issuance for the class + pub total_issuance: TokenId, + /// Class owner + pub owner: AccountId, + /// Class Properties + pub data: Data, + } -/// Token info -#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] -pub struct TokenInfo<AccountId, Data> { - /// Token metadata - pub metadata: Vec<u8>, - /// Token owner - pub owner: AccountId, - /// Token Properties - pub data: Data, -} + /// Token info + #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] + pub struct TokenInfo<AccountId, Data> { + /// Token metadata + pub metadata: Vec<u8>, + /// Token owner + pub owner: AccountId, + /// Token Properties + pub data: Data, + } -pub trait Config: frame_system::Config { - /// The class ID type - type ClassId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy; - /// The token ID type - type TokenId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy; - /// The class properties type - type ClassData: Parameter + Member; - /// The token properties type - type TokenData: Parameter + Member; -} + #[pallet::config] + pub trait Config: frame_system::Config { + /// The class ID type + type ClassId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy; + /// The token ID type + type TokenId: Parameter + Member + AtLeast32BitUnsigned + Default + Copy; + /// The class properties type + type ClassData: Parameter + Member + MaybeSerializeDeserialize; + /// The token properties type + type TokenData: Parameter + Member + MaybeSerializeDeserialize; + } + + pub type ClassInfoOf<T> = + ClassInfo<<T as Config>::TokenId, <T as frame_system::Config>::AccountId, <T as Config>::ClassData>; + pub type TokenInfoOf<T> = TokenInfo<<T as frame_system::Config>::AccountId, <T as Config>::TokenData>; + + pub type GenesisTokenData<T> = ( + <T as frame_system::Config>::AccountId, // Token owner + Vec<u8>, // Token metadata + <T as Config>::TokenData, + ); + pub type GenesisTokens<T> = ( + <T as frame_system::Config>::AccountId, // Token class owner + Vec<u8>, // Token class metadata + <T as Config>::ClassData, + Vec<GenesisTokenData<T>>, // Vector of tokens belonging to this class + ); + + #[pallet::pallet] + pub struct Pallet<T>(PhantomData<T>); -decl_error! { /// Error for non-fungible-token module. - pub enum Error for Module<T: Config> { + #[pallet::error] + pub enum Error<T> { /// No available class ID NoAvailableClassId, /// No available token ID @@ -85,182 +110,193 @@ decl_error! { /// Total issuance is not 0 CannotDestroyClass, } -} -pub type ClassInfoOf<T> = - ClassInfo<<T as Config>::TokenId, <T as frame_system::Config>::AccountId, <T as Config>::ClassData>; -pub type TokenInfoOf<T> = TokenInfo<<T as frame_system::Config>::AccountId, <T as Config>::TokenData>; - -pub type GenesisTokenData<T> = ( - <T as frame_system::Config>::AccountId, // Token owner - Vec<u8>, // Token metadata - <T as Config>::TokenData, -); -pub type GenesisTokens<T> = ( - <T as frame_system::Config>::AccountId, // Token class owner - Vec<u8>, // Token class metadata - <T as Config>::ClassData, - Vec<GenesisTokenData<T>>, // Vector of tokens belonging to this class -); - -decl_storage! { - trait Store for Module<T: Config> as NonFungibleToken { - /// Next available class ID. - pub NextClassId get(fn next_class_id): T::ClassId; - /// Next available token ID. - pub NextTokenId get(fn next_token_id): map hasher(twox_64_concat) T::ClassId => T::TokenId; - /// Store class info. - /// - /// Returns `None` if class info not set or removed. - pub Classes get(fn classes): map hasher(twox_64_concat) T::ClassId => Option<ClassInfoOf<T>>; - /// Store token info. - /// - /// Returns `None` if token info not set or removed. - pub Tokens get(fn tokens): double_map hasher(twox_64_concat) T::ClassId, hasher(twox_64_concat) T::TokenId => Option<TokenInfoOf<T>>; - /// Token existence check by owner and class ID. - #[cfg(not(feature = "disable-tokens-by-owner"))] - pub TokensByOwner get(fn tokens_by_owner): double_map hasher(twox_64_concat) T::AccountId, hasher(twox_64_concat) (T::ClassId, T::TokenId) => Option<()>; + /// Next available class ID. + #[pallet::storage] + #[pallet::getter(fn next_class_id)] + pub type NextClassId<T: Config> = StorageValue<_, T::ClassId, ValueQuery>; + + /// Next available token ID. + #[pallet::storage] + #[pallet::getter(fn next_token_id)] + pub type NextTokenId<T: Config> = StorageMap<_, Twox64Concat, T::ClassId, T::TokenId, ValueQuery>; + + /// Store class info. + /// + /// Returns `None` if class info not set or removed. + #[pallet::storage] + #[pallet::getter(fn classes)] + pub type Classes<T: Config> = StorageMap<_, Twox64Concat, T::ClassId, ClassInfoOf<T>>; + + /// Store token info. + /// + /// Returns `None` if token info not set or removed. + #[pallet::storage] + #[pallet::getter(fn tokens)] + pub type Tokens<T: Config> = + StorageDoubleMap<_, Twox64Concat, T::ClassId, Twox64Concat, T::TokenId, TokenInfoOf<T>>; + + /// Token existence check by owner and class ID. + // TODO: pallet macro doesn't support conditional compiling. Always having `TokensByOwner` storage doesn't hurt but + // it could be removed once conditional compiling supported. + // #[cfg(not(feature = "disable-tokens-by-owner"))] + #[pallet::storage] + #[pallet::getter(fn tokens_by_owner)] + pub type TokensByOwner<T: Config> = + StorageDoubleMap<_, Twox64Concat, T::AccountId, Twox64Concat, (T::ClassId, T::TokenId), (), ValueQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig<T: Config> { + pub tokens: Vec<GenesisTokens<T>>, + } + + #[cfg(feature = "std")] + impl<T: Config> Default for GenesisConfig<T> { + fn default() -> Self { + GenesisConfig { tokens: vec![] } + } } - add_extra_genesis { - config(tokens): Vec<GenesisTokens<T>>; - build(|config: &GenesisConfig<T>| { - config.tokens.iter().for_each(|token_class| { - let class_id = Module::<T>::create_class(&token_class.0, token_class.1.to_vec(), token_class.2.clone()) + #[pallet::genesis_build] + impl<T: Config> GenesisBuild<T> for GenesisConfig<T> { + fn build(&self) { + self.tokens.iter().for_each(|token_class| { + let class_id = Pallet::<T>::create_class(&token_class.0, token_class.1.to_vec(), token_class.2.clone()) .expect("Create class cannot fail while building genesis"); for (account_id, token_metadata, token_data) in &token_class.3 { - Module::<T>::mint(&account_id, class_id, token_metadata.to_vec(), token_data.clone()) + Pallet::<T>::mint(&account_id, class_id, token_metadata.to_vec(), token_data.clone()) .expect("Token mint cannot fail during genesis"); } }) - }) + } } -} - -decl_module! { - pub struct Module<T: Config> for enum Call where origin: T::Origin { - } -} - -impl<T: Config> Module<T> { - /// Create NFT(non fungible token) class - pub fn create_class( - owner: &T::AccountId, - metadata: Vec<u8>, - data: T::ClassData, - ) -> Result<T::ClassId, DispatchError> { - let class_id = NextClassId::<T>::try_mutate(|id| -> Result<T::ClassId, DispatchError> { - let current_id = *id; - *id = id.checked_add(&One::one()).ok_or(Error::<T>::NoAvailableClassId)?; - Ok(current_id) - })?; - - let info = ClassInfo { - metadata, - total_issuance: Default::default(), - owner: owner.clone(), - data, - }; - Classes::<T>::insert(class_id, info); - - Ok(class_id) - } - - /// Transfer NFT(non fungible token) from `from` account to `to` account - pub fn transfer(from: &T::AccountId, to: &T::AccountId, token: (T::ClassId, T::TokenId)) -> DispatchResult { - Tokens::<T>::try_mutate(token.0, token.1, |token_info| -> DispatchResult { - let mut info = token_info.as_mut().ok_or(Error::<T>::TokenNotFound)?; - ensure!(info.owner == *from, Error::<T>::NoPermission); - if from == to { - // no change needed - return Ok(()); - } - - info.owner = to.clone(); - - #[cfg(not(feature = "disable-tokens-by-owner"))] - { - TokensByOwner::<T>::remove(from, token); - TokensByOwner::<T>::insert(to, token, ()); - } - Ok(()) - }) - } - - /// Mint NFT(non fungible token) to `owner` - pub fn mint( - owner: &T::AccountId, - class_id: T::ClassId, - metadata: Vec<u8>, - data: T::TokenData, - ) -> Result<T::TokenId, DispatchError> { - NextTokenId::<T>::try_mutate(class_id, |id| -> Result<T::TokenId, DispatchError> { - let token_id = *id; - *id = id.checked_add(&One::one()).ok_or(Error::<T>::NoAvailableTokenId)?; - - Classes::<T>::try_mutate(class_id, |class_info| -> DispatchResult { - let info = class_info.as_mut().ok_or(Error::<T>::ClassNotFound)?; - info.total_issuance = info - .total_issuance - .checked_add(&One::one()) - .ok_or(Error::<T>::NumOverflow)?; - Ok(()) + #[pallet::hooks] + impl<T: Config> Hooks<T::BlockNumber> for Pallet<T> {} + + #[pallet::call] + impl<T: Config> Pallet<T> {} + + impl<T: Config> Pallet<T> { + /// Create NFT(non fungible token) class + pub fn create_class( + owner: &T::AccountId, + metadata: Vec<u8>, + data: T::ClassData, + ) -> Result<T::ClassId, DispatchError> { + let class_id = NextClassId::<T>::try_mutate(|id| -> Result<T::ClassId, DispatchError> { + let current_id = *id; + *id = id.checked_add(&One::one()).ok_or(Error::<T>::NoAvailableClassId)?; + Ok(current_id) })?; - let token_info = TokenInfo { + let info = ClassInfo { metadata, + total_issuance: Default::default(), owner: owner.clone(), data, }; - Tokens::<T>::insert(class_id, token_id, token_info); - #[cfg(not(feature = "disable-tokens-by-owner"))] - TokensByOwner::<T>::insert(owner, (class_id, token_id), ()); + Classes::<T>::insert(class_id, info); + + Ok(class_id) + } + + /// Transfer NFT(non fungible token) from `from` account to `to` account + pub fn transfer(from: &T::AccountId, to: &T::AccountId, token: (T::ClassId, T::TokenId)) -> DispatchResult { + Tokens::<T>::try_mutate(token.0, token.1, |token_info| -> DispatchResult { + let mut info = token_info.as_mut().ok_or(Error::<T>::TokenNotFound)?; + ensure!(info.owner == *from, Error::<T>::NoPermission); + if from == to { + // no change needed + return Ok(()); + } - Ok(token_id) - }) - } + info.owner = to.clone(); - /// Burn NFT(non fungible token) from `owner` - pub fn burn(owner: &T::AccountId, token: (T::ClassId, T::TokenId)) -> DispatchResult { - Tokens::<T>::try_mutate_exists(token.0, token.1, |token_info| -> DispatchResult { - let t = token_info.take().ok_or(Error::<T>::TokenNotFound)?; - ensure!(t.owner == *owner, Error::<T>::NoPermission); - - Classes::<T>::try_mutate(token.0, |class_info| -> DispatchResult { - let info = class_info.as_mut().ok_or(Error::<T>::ClassNotFound)?; - info.total_issuance = info - .total_issuance - .checked_sub(&One::one()) - .ok_or(Error::<T>::NumOverflow)?; - Ok(()) - })?; + #[cfg(not(feature = "disable-tokens-by-owner"))] + { + TokensByOwner::<T>::remove(from, token); + TokensByOwner::<T>::insert(to, token, ()); + } - #[cfg(not(feature = "disable-tokens-by-owner"))] - TokensByOwner::<T>::remove(owner, token); + Ok(()) + }) + } + + /// Mint NFT(non fungible token) to `owner` + pub fn mint( + owner: &T::AccountId, + class_id: T::ClassId, + metadata: Vec<u8>, + data: T::TokenData, + ) -> Result<T::TokenId, DispatchError> { + NextTokenId::<T>::try_mutate(class_id, |id| -> Result<T::TokenId, DispatchError> { + let token_id = *id; + *id = id.checked_add(&One::one()).ok_or(Error::<T>::NoAvailableTokenId)?; + + Classes::<T>::try_mutate(class_id, |class_info| -> DispatchResult { + let info = class_info.as_mut().ok_or(Error::<T>::ClassNotFound)?; + info.total_issuance = info + .total_issuance + .checked_add(&One::one()) + .ok_or(Error::<T>::NumOverflow)?; + Ok(()) + })?; + + let token_info = TokenInfo { + metadata, + owner: owner.clone(), + data, + }; + Tokens::<T>::insert(class_id, token_id, token_info); + #[cfg(not(feature = "disable-tokens-by-owner"))] + TokensByOwner::<T>::insert(owner, (class_id, token_id), ()); + + Ok(token_id) + }) + } + + /// Burn NFT(non fungible token) from `owner` + pub fn burn(owner: &T::AccountId, token: (T::ClassId, T::TokenId)) -> DispatchResult { + Tokens::<T>::try_mutate_exists(token.0, token.1, |token_info| -> DispatchResult { + let t = token_info.take().ok_or(Error::<T>::TokenNotFound)?; + ensure!(t.owner == *owner, Error::<T>::NoPermission); + + Classes::<T>::try_mutate(token.0, |class_info| -> DispatchResult { + let info = class_info.as_mut().ok_or(Error::<T>::ClassNotFound)?; + info.total_issuance = info + .total_issuance + .checked_sub(&One::one()) + .ok_or(Error::<T>::NumOverflow)?; + Ok(()) + })?; + + #[cfg(not(feature = "disable-tokens-by-owner"))] + TokensByOwner::<T>::remove(owner, token); - Ok(()) - }) - } + Ok(()) + }) + } - /// Destroy NFT(non fungible token) class - pub fn destroy_class(owner: &T::AccountId, class_id: T::ClassId) -> DispatchResult { - Classes::<T>::try_mutate_exists(class_id, |class_info| -> DispatchResult { - let info = class_info.take().ok_or(Error::<T>::ClassNotFound)?; - ensure!(info.owner == *owner, Error::<T>::NoPermission); - ensure!(info.total_issuance == Zero::zero(), Error::<T>::CannotDestroyClass); + /// Destroy NFT(non fungible token) class + pub fn destroy_class(owner: &T::AccountId, class_id: T::ClassId) -> DispatchResult { + Classes::<T>::try_mutate_exists(class_id, |class_info| -> DispatchResult { + let info = class_info.take().ok_or(Error::<T>::ClassNotFound)?; + ensure!(info.owner == *owner, Error::<T>::NoPermission); + ensure!(info.total_issuance == Zero::zero(), Error::<T>::CannotDestroyClass); - NextTokenId::<T>::remove(class_id); + NextTokenId::<T>::remove(class_id); - Ok(()) - }) - } + Ok(()) + }) + } - pub fn is_owner(account: &T::AccountId, token: (T::ClassId, T::TokenId)) -> bool { - #[cfg(feature = "disable-tokens-by-owner")] - return Tokens::<T>::get(token.0, token.1).map_or(false, |token| token.owner == *account); + pub fn is_owner(account: &T::AccountId, token: (T::ClassId, T::TokenId)) -> bool { + #[cfg(feature = "disable-tokens-by-owner")] + return Tokens::<T>::get(token.0, token.1).map_or(false, |token| token.owner == *account); - #[cfg(not(feature = "disable-tokens-by-owner"))] - TokensByOwner::<T>::contains_key(account, token) + #[cfg(not(feature = "disable-tokens-by-owner"))] + TokensByOwner::<T>::contains_key(account, token) + } } } diff --git a/oracle/src/default_combine_data.rs b/oracle/src/default_combine_data.rs index 5c097e4..4f096cf 100644 --- a/oracle/src/default_combine_data.rs +++ b/oracle/src/default_combine_data.rs @@ -1,19 +1,17 @@ -use crate::{Config, DefaultInstance, Instance, MomentOf, TimestampedValueOf}; +use crate::{Config, MomentOf, TimestampedValueOf}; use frame_support::traits::{Get, Time}; use orml_traits::CombineData; use sp_std::{marker, prelude::*}; /// Sort by value and returns median timestamped value. /// Returns prev_value if not enough valid values. -pub struct DefaultCombineData<T, MinimumCount, ExpiresIn, I = DefaultInstance>( - marker::PhantomData<(T, I, MinimumCount, ExpiresIn)>, -); +pub struct DefaultCombineData<T, MinimumCount, ExpiresIn, I = ()>(marker::PhantomData<(T, I, MinimumCount, ExpiresIn)>); impl<T, I, MinimumCount, ExpiresIn> CombineData<<T as Config<I>>::OracleKey, TimestampedValueOf<T, I>> for DefaultCombineData<T, MinimumCount, ExpiresIn, I> where T: Config<I>, - I: Instance, + I: 'static, MinimumCount: Get<u32>, ExpiresIn: Get<MomentOf<T, I>>, { diff --git a/oracle/src/lib.rs b/oracle/src/lib.rs index 6a34b75..58e6062 100644 --- a/oracle/src/lib.rs +++ b/oracle/src/lib.rs @@ -19,274 +19,312 @@ #![cfg_attr(not(feature = "std"), no_std)] // Disable the following two lints since they originate from an external macro (namely decl_storage) #![allow(clippy::string_lit_as_bytes)] +#![allow(clippy::unused_unit)] mod default_combine_data; mod default_weight; mod mock; mod tests; -pub trait WeightInfo { - fn feed_values(c: u32) -> Weight; - fn on_finalize() -> Weight; -} +pub use module::*; -use codec::{Decode, Encode}; -pub use default_combine_data::DefaultCombineData; -use frame_support::{ - decl_error, decl_event, decl_module, decl_storage, - dispatch::DispatchResultWithPostInfo, - ensure, - traits::{ChangeMembers, Get, InitializeMembers, Time}, - weights::{DispatchClass, Pays, Weight}, - IterableStorageMap, Parameter, -}; -use frame_system::{ensure_root, ensure_signed}; -pub use orml_traits::{CombineData, DataFeeder, DataProvider, DataProviderExtended, OnNewData}; -use orml_utilities::OrderedSet; -#[cfg(feature = "std")] -use serde::{Deserialize, Serialize}; -use sp_runtime::{traits::Member, DispatchResult, RuntimeDebug}; -use sp_std::{prelude::*, vec}; - -type MomentOf<T, I = DefaultInstance> = <<T as Config<I>>::Time as Time>::Moment; -pub type TimestampedValueOf<T, I = DefaultInstance> = TimestampedValue<<T as Config<I>>::OracleValue, MomentOf<T, I>>; - -#[derive(Encode, Decode, RuntimeDebug, Eq, PartialEq, Clone, Copy, Ord, PartialOrd)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct TimestampedValue<Value, Moment> { - pub value: Value, - pub timestamp: Moment, -} +#[frame_support::pallet] +pub mod module { + use codec::{Decode, Encode}; + + use frame_support::{ + ensure, + pallet_prelude::*, + traits::{ChangeMembers, Get, InitializeMembers, Time}, + weights::{Pays, Weight}, + Parameter, + }; + use frame_system::{ensure_root, ensure_signed, pallet_prelude::*}; + pub use orml_traits::{CombineData, DataFeeder, DataProvider, DataProviderExtended, OnNewData}; + use orml_utilities::OrderedSet; + + #[cfg(feature = "std")] + use serde::{Deserialize, Serialize}; -pub trait Config<I: Instance = DefaultInstance>: frame_system::Config { - type Event: From<Event<Self, I>> + Into<<Self as frame_system::Config>::Event>; + use sp_runtime::{traits::Member, DispatchResult, RuntimeDebug}; + use sp_std::{prelude::*, vec}; - /// Hook on new data received - type OnNewData: OnNewData<Self::AccountId, Self::OracleKey, Self::OracleValue>; + pub use crate::default_combine_data::DefaultCombineData; - /// Provide the implementation to combine raw values to produce aggregated - /// value - type CombineData: CombineData<Self::OracleKey, TimestampedValueOf<Self, I>>; + pub trait WeightInfo { + fn feed_values(c: u32) -> Weight; + fn on_finalize() -> Weight; + } + + pub type MomentOf<T, I = ()> = <<T as Config<I>>::Time as Time>::Moment; + pub type TimestampedValueOf<T, I = ()> = TimestampedValue<<T as Config<I>>::OracleValue, MomentOf<T, I>>; - /// Time provider - type Time: Time; + #[derive(Encode, Decode, RuntimeDebug, Eq, PartialEq, Clone, Copy, Ord, PartialOrd)] + #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] + pub struct TimestampedValue<Value, Moment> { + pub value: Value, + pub timestamp: Moment, + } - /// The data key type - type OracleKey: Parameter + Member; + #[pallet::config] + pub trait Config<I: 'static = ()>: frame_system::Config { + type Event: From<Event<Self, I>> + IsType<<Self as frame_system::Config>::Event>; - /// The data value type - type OracleValue: Parameter + Member + Ord; + /// Hook on new data received + type OnNewData: OnNewData<Self::AccountId, Self::OracleKey, Self::OracleValue>; - /// The root operator account id, recorad all sudo feeds on this account. - type RootOperatorAccountId: Get<Self::AccountId>; + /// Provide the implementation to combine raw values to produce + /// aggregated value + type CombineData: CombineData<Self::OracleKey, TimestampedValueOf<Self, I>>; - /// Weight information for extrinsics in this module. - type WeightInfo: WeightInfo; -} + /// Time provider + type Time: Time; -decl_error! { - pub enum Error for Module<T: Config<I>, I: Instance> { + /// The data key type + type OracleKey: Parameter + Member; + + /// The data value type + type OracleValue: Parameter + Member + Ord; + + /// The root operator account id, record all sudo feeds on this account. + type RootOperatorAccountId: Get<Self::AccountId>; + + /// Weight information for extrinsics in this module. + type WeightInfo: WeightInfo; + } + + #[pallet::pallet] + pub struct Pallet<T, I = ()>(PhantomData<(T, I)>); + + #[pallet::error] + pub enum Error<T, I = ()> { /// Sender does not have permission NoPermission, /// Feeder has already feeded at this block AlreadyFeeded, } -} -decl_event!( - pub enum Event<T, I=DefaultInstance> where - <T as frame_system::Config>::AccountId, - <T as Config<I>>::OracleKey, - <T as Config<I>>::OracleValue, - { + #[pallet::event] + #[pallet::generate_deposit(fn deposit_event)] + pub enum Event<T: Config<I>, I: 'static = ()> { /// New feed data is submitted. [sender, values] - NewFeedData(AccountId, Vec<(OracleKey, OracleValue)>), + NewFeedData(T::AccountId, Vec<(T::OracleKey, T::OracleValue)>), } -); - -decl_storage! { - trait Store for Module<T: Config<I>, I: Instance=DefaultInstance> as Oracle { - /// Raw values for each oracle operators - pub RawValues get(fn raw_values): double_map hasher(twox_64_concat) T::AccountId, hasher(twox_64_concat) T::OracleKey => Option<TimestampedValueOf<T, I>>; - - /// True if Self::values(key) is up to date, otherwise the value is stale - pub IsUpdated get(fn is_updated): map hasher(twox_64_concat) <T as Config<I>>::OracleKey => bool; - - /// Combined value, may not be up to date - pub Values get(fn values): map hasher(twox_64_concat) <T as Config<I>>::OracleKey => Option<TimestampedValueOf<T, I>>; - - /// If an oracle operator has feed a value in this block - HasDispatched: OrderedSet<T::AccountId>; - - // TODO: this shouldn't be required https://github.com/paritytech/substrate/issues/6041 - /// The current members of the collective. This is stored sorted (just by value). - pub Members get(fn members) config(): OrderedSet<T::AccountId>; + /// Raw values for each oracle operators + #[pallet::storage] + #[pallet::getter(fn raw_values)] + pub type RawValues<T: Config<I>, I: 'static = ()> = + StorageDoubleMap<_, Twox64Concat, T::AccountId, Twox64Concat, T::OracleKey, TimestampedValueOf<T, I>>; + + /// True if Self::values(key) is up to date, otherwise the value is stale + #[pallet::storage] + #[pallet::getter(fn is_updated)] + pub type IsUpdated<T: Config<I>, I: 'static = ()> = + StorageMap<_, Twox64Concat, <T as Config<I>>::OracleKey, bool, ValueQuery>; + + /// Combined value, may not be up to date + #[pallet::storage] + #[pallet::getter(fn values)] + pub type Values<T: Config<I>, I: 'static = ()> = + StorageMap<_, Twox64Concat, <T as Config<I>>::OracleKey, TimestampedValueOf<T, I>>; + + /// If an oracle operator has feed a value in this block + #[pallet::storage] + type HasDispatched<T: Config<I>, I: 'static = ()> = StorageValue<_, OrderedSet<T::AccountId>, ValueQuery>; + + // TODO: this shouldn't be required https://github.com/paritytech/substrate/issues/6041 + /// The current members of the collective. This is stored sorted (just by + /// value). + #[pallet::storage] + #[pallet::getter(fn members)] + pub type Members<T: Config<I>, I: 'static = ()> = StorageValue<_, OrderedSet<T::AccountId>, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn nonces)] + pub type Nonces<T: Config<I>, I: 'static = ()> = StorageMap<_, Twox64Concat, T::AccountId, u32>; + + #[pallet::genesis_config] + pub struct GenesisConfig<T: Config<I>, I: 'static = ()> { + pub members: OrderedSet<T::AccountId>, + pub phantom: sp_std::marker::PhantomData<I>, + } - pub Nonces get(fn nonces): map hasher(twox_64_concat) T::AccountId => u32; + #[cfg(feature = "std")] + impl<T: Config<I>, I: 'static> Default for GenesisConfig<T, I> { + fn default() -> Self { + GenesisConfig { + members: Default::default(), + phantom: Default::default(), + } + } } - add_extra_genesis { - config(phantom): sp_std::marker::PhantomData<I>; + #[pallet::genesis_build] + impl<T: Config<I>, I: 'static> GenesisBuild<T, I> for GenesisConfig<T, I> { + fn build(&self) { + <Members<T, I>>::put(self.members.clone()); + } } -} -decl_module! { - pub struct Module<T: Config<I>, I: Instance=DefaultInstance> for enum Call where origin: T::Origin { - type Error = Error<T, I>; + #[pallet::hooks] + impl<T: Config<I>, I: 'static> Hooks<T::BlockNumber> for Pallet<T, I> { + /// `on_initialize` to return the weight used in `on_finalize`. + fn on_initialize(_n: T::BlockNumber) -> Weight { + T::WeightInfo::on_finalize() + } - fn deposit_event() = default; + fn on_finalize(_n: T::BlockNumber) { + // cleanup for next block + <HasDispatched<T, I>>::kill(); + } + } + #[pallet::call] + impl<T: Config<I>, I: 'static> Pallet<T, I> { /// Feed the external value. /// /// Require authorized operator. - #[weight = (T::WeightInfo::feed_values(values.len() as u32), DispatchClass::Operational)] + #[pallet::weight(T::WeightInfo::feed_values(values.len() as u32))] pub fn feed_values( - origin, + origin: OriginFor<T>, values: Vec<(T::OracleKey, T::OracleValue)>, ) -> DispatchResultWithPostInfo { - let feeder = ensure_signed(origin.clone()).or_else(|_| ensure_root(origin).map(|_| T::RootOperatorAccountId::get()))?; + let feeder = ensure_signed(origin.clone()) + .or_else(|_| ensure_root(origin).map(|_| T::RootOperatorAccountId::get()))?; Self::do_feed_values(feeder, values)?; Ok(Pays::No.into()) } + } - /// `on_initialize` to return the weight used in `on_finalize`. - fn on_initialize() -> Weight { - T::WeightInfo::on_finalize() + impl<T: Config<I>, I: 'static> Pallet<T, I> { + pub fn read_raw_values(key: &T::OracleKey) -> Vec<TimestampedValueOf<T, I>> { + Self::members() + .0 + .iter() + .chain(vec![T::RootOperatorAccountId::get()].iter()) + .filter_map(|x| Self::raw_values(x, key)) + .collect() } - fn on_finalize(_n: T::BlockNumber) { - // cleanup for next block - <HasDispatched<T, I>>::kill(); + /// Returns fresh combined value if has update, or latest combined + /// value. + /// + /// Note this will update values storage if has update. + pub fn get(key: &T::OracleKey) -> Option<TimestampedValueOf<T, I>> { + if Self::is_updated(key) { + <Values<T, I>>::get(key) + } else { + let timestamped = Self::combined(key)?; + <Values<T, I>>::insert(key, timestamped.clone()); + IsUpdated::<T, I>::insert(key, true); + Some(timestamped) + } } - } -} - -impl<T: Config<I>, I: Instance> Module<T, I> { - pub fn read_raw_values(key: &T::OracleKey) -> Vec<TimestampedValueOf<T, I>> { - Self::members() - .0 - .iter() - .chain(vec![T::RootOperatorAccountId::get()].iter()) - .filter_map(|x| Self::raw_values(x, key)) - .collect() - } - /// Returns fresh combined value if has update, or latest combined value. - /// - /// Note this will update values storage if has update. - pub fn get(key: &T::OracleKey) -> Option<TimestampedValueOf<T, I>> { - if Self::is_updated(key) { - <Values<T, I>>::get(key) - } else { - let timestamped = Self::combined(key)?; - <Values<T, I>>::insert(key, timestamped.clone()); - IsUpdated::<T, I>::insert(key, true); - Some(timestamped) + /// Returns fresh combined value if has update, or latest combined + /// value. + /// + /// This is a no-op function which would not change storage. + pub fn get_no_op(key: &T::OracleKey) -> Option<TimestampedValueOf<T, I>> { + if Self::is_updated(key) { + Self::values(key) + } else { + Self::combined(key) + } } - } - /// Returns fresh combined value if has update, or latest combined value. - /// - /// This is a no-op function which would not change storage. - pub fn get_no_op(key: &T::OracleKey) -> Option<TimestampedValueOf<T, I>> { - if Self::is_updated(key) { - Self::values(key) - } else { - Self::combined(key) + #[allow(clippy::complexity)] + pub fn get_all_values() -> Vec<(T::OracleKey, Option<TimestampedValueOf<T, I>>)> { + <Values<T, I>>::iter() + .map(|(key, _)| key) + .map(|key| { + let v = Self::get_no_op(&key); + (key, v) + }) + .collect() } - } - - #[allow(clippy::complexity)] - pub fn get_all_values() -> Vec<(T::OracleKey, Option<TimestampedValueOf<T, I>>)> { - <Values<T, I>>::iter() - .map(|(key, _)| key) - .map(|key| { - let v = Self::get_no_op(&key); - (key, v) - }) - .collect() - } - fn combined(key: &T::OracleKey) -> Option<TimestampedValueOf<T, I>> { - let values = Self::read_raw_values(key); - T::CombineData::combine_data(key, values, Self::values(key)) - } + fn combined(key: &T::OracleKey) -> Option<TimestampedValueOf<T, I>> { + let values = Self::read_raw_values(key); + T::CombineData::combine_data(key, values, Self::values(key)) + } - fn do_feed_values(who: T::AccountId, values: Vec<(T::OracleKey, T::OracleValue)>) -> DispatchResult { - // ensure feeder is authorized - ensure!( - Self::members().contains(&who) || who == T::RootOperatorAccountId::get(), - Error::<T, I>::NoPermission - ); - - // ensure account hasn't dispatched an updated yet - ensure!( - HasDispatched::<T, I>::mutate(|set| set.insert(who.clone())), - Error::<T, I>::AlreadyFeeded - ); - - let now = T::Time::now(); - for (key, value) in &values { - let timestamped = TimestampedValue { - value: value.clone(), - timestamp: now, - }; - RawValues::<T, I>::insert(&who, &key, timestamped); - IsUpdated::<T, I>::remove(&key); - - T::OnNewData::on_new_data(&who, &key, &value); + fn do_feed_values(who: T::AccountId, values: Vec<(T::OracleKey, T::OracleValue)>) -> DispatchResult { + // ensure feeder is authorized + ensure!( + Self::members().contains(&who) || who == T::RootOperatorAccountId::get(), + Error::<T, I>::NoPermission + ); + + // ensure account hasn't dispatched an updated yet + ensure!( + HasDispatched::<T, I>::mutate(|set| set.insert(who.clone())), + Error::<T, I>::AlreadyFeeded + ); + + let now = T::Time::now(); + for (key, value) in &values { + let timestamped = TimestampedValue { + value: value.clone(), + timestamp: now, + }; + RawValues::<T, I>::insert(&who, &key, timestamped); + IsUpdated::<T, I>::remove(&key); + + T::OnNewData::on_new_data(&who, &key, &value); + } + Self::deposit_event(Event::NewFeedData(who, values)); + Ok(()) } - Self::deposit_event(RawEvent::NewFeedData(who, values)); - Ok(()) } -} -impl<T: Config<I>, I: Instance> InitializeMembers<T::AccountId> for Module<T, I> { - fn initialize_members(members: &[T::AccountId]) { - if !members.is_empty() { - assert!(Members::<T, I>::get().0.is_empty(), "Members are already initialized!"); - Members::<T, I>::put(OrderedSet::from_sorted_set(members.into())); + impl<T: Config<I>, I: 'static> InitializeMembers<T::AccountId> for Pallet<T, I> { + fn initialize_members(members: &[T::AccountId]) { + if !members.is_empty() { + assert!(Members::<T, I>::get().0.is_empty(), "Members are already initialized!"); + Members::<T, I>::put(OrderedSet::from_sorted_set(members.into())); + } } } -} -impl<T: Config<I>, I: Instance> ChangeMembers<T::AccountId> for Module<T, I> { - fn change_members_sorted(_incoming: &[T::AccountId], outgoing: &[T::AccountId], new: &[T::AccountId]) { - // remove session keys and its values - for removed in outgoing { - RawValues::<T, I>::remove_prefix(removed); - } + impl<T: Config<I>, I: 'static> ChangeMembers<T::AccountId> for Pallet<T, I> { + fn change_members_sorted(_incoming: &[T::AccountId], outgoing: &[T::AccountId], new: &[T::AccountId]) { + // remove session keys and its values + for removed in outgoing { + RawValues::<T, I>::remove_prefix(removed); + } - Members::<T, I>::put(OrderedSet::from_sorted_set(new.into())); + Members::<T, I>::put(OrderedSet::from_sorted_set(new.into())); - // not bothering to track which key needs recompute, just update all - IsUpdated::<T, I>::remove_all(); - } + // not bothering to track which key needs recompute, just update all + IsUpdated::<T, I>::remove_all(); + } - fn set_prime(_prime: Option<T::AccountId>) { - // nothing + fn set_prime(_prime: Option<T::AccountId>) { + // nothing + } } -} -impl<T: Config<I>, I: Instance> DataProvider<T::OracleKey, T::OracleValue> for Module<T, I> { - fn get(key: &T::OracleKey) -> Option<T::OracleValue> { - Self::get(key).map(|timestamped_value| timestamped_value.value) - } -} -impl<T: Config<I>, I: Instance> DataProviderExtended<T::OracleKey, TimestampedValueOf<T, I>> for Module<T, I> { - fn get_no_op(key: &T::OracleKey) -> Option<TimestampedValueOf<T, I>> { - Self::get_no_op(key) + impl<T: Config<I>, I: 'static> DataProvider<T::OracleKey, T::OracleValue> for Pallet<T, I> { + fn get(key: &T::OracleKey) -> Option<T::OracleValue> { + Self::get(key).map(|timestamped_value| timestamped_value.value) + } } - #[allow(clippy::complexity)] - fn get_all_values() -> Vec<(T::OracleKey, Option<TimestampedValueOf<T, I>>)> { - Self::get_all_values() + impl<T: Config<I>, I: 'static> DataProviderExtended<T::OracleKey, TimestampedValueOf<T, I>> for Pallet<T, I> { + fn get_no_op(key: &T::OracleKey) -> Option<TimestampedValueOf<T, I>> { + Self::get_no_op(key) + } + #[allow(clippy::complexity)] + fn get_all_values() -> Vec<(T::OracleKey, Option<TimestampedValueOf<T, I>>)> { + Self::get_all_values() + } } -} -impl<T: Config<I>, I: Instance> DataFeeder<T::OracleKey, T::OracleValue, T::AccountId> for Module<T, I> { - fn feed_value(who: T::AccountId, key: T::OracleKey, value: T::OracleValue) -> DispatchResult { - Self::do_feed_values(who, vec![(key, value)])?; - Ok(()) + impl<T: Config<I>, I: 'static> DataFeeder<T::OracleKey, T::OracleValue, T::AccountId> for Pallet<T, I> { + fn feed_value(who: T::AccountId, key: T::OracleKey, value: T::OracleValue) -> DispatchResult { + Self::do_feed_values(who, vec![(key, value)])?; + Ok(()) + } } } diff --git a/oracle/src/mock.rs b/oracle/src/mock.rs index a35cbb8..d9fc427 100644 --- a/oracle/src/mock.rs +++ b/oracle/src/mock.rs @@ -2,7 +2,10 @@ use super::*; -use frame_support::{impl_outer_dispatch, impl_outer_event, impl_outer_origin, parameter_types}; +use frame_support::{ + impl_outer_dispatch, impl_outer_event, impl_outer_origin, parameter_types, + traits::{GenesisBuild, Time}, +}; use sp_core::H256; use sp_runtime::{ testing::Header, diff --git a/oracle/src/tests.rs b/oracle/src/tests.rs index 270203c..adbdf93 100644 --- a/oracle/src/tests.rs +++ b/oracle/src/tests.rs @@ -1,14 +1,12 @@ #![cfg(test)] -use crate::{ - mock::{new_test_ext, AccountId, ModuleOracle, Origin, RootOperatorAccountId, System, Test, TestEvent, Timestamp}, - Error, RawEvent, TimestampedValue, -}; +use super::*; use frame_support::{ assert_noop, assert_ok, traits::{ChangeMembers, OnFinalize}, weights::Pays, }; +use mock::*; #[test] fn should_feed_values_from_member() { @@ -28,7 +26,7 @@ fn should_feed_values_from_member() { Pays::No ); - let new_feed_data_event = TestEvent::oracle(RawEvent::NewFeedData(1, vec![(50, 1000), (51, 900), (52, 800)])); + let new_feed_data_event = TestEvent::oracle(Event::NewFeedData(1, vec![(50, 1000), (51, 900), (52, 800)])); assert!(System::events() .iter() .any(|record| record.event == new_feed_data_event)); diff --git a/utilities/src/ordered_set.rs b/utilities/src/ordered_set.rs index e5143c0..86e9d38 100644 --- a/utilities/src/ordered_set.rs +++ b/utilities/src/ordered_set.rs @@ -6,7 +6,7 @@ use sp_std::prelude::*; /// An ordered set backed by `Vec` #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -#[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Default)] +#[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Default, Clone)] pub struct OrderedSet<T>(pub Vec<T>); impl<T: Ord> OrderedSet<T> { -- GitLab