#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
pub mod weights;
extern crate alloc;
pub use weights::WeightInfo;
use codec::{Decode, DecodeWithMemTracking, Encode};
use pezframe_support::{
dispatch::{DispatchInfo, PostDispatchInfo},
pezpallet_prelude::{Pays, Zero},
traits::{ContainsPair, OriginTrait},
weights::WeightToFee,
Parameter, RuntimeDebugNoBound,
};
use pezframe_system::pezpallet_prelude::BlockNumberFor;
use pezpallet_transaction_payment::OnChargeTransaction;
use pezsp_runtime::{
traits::{
AsTransactionAuthorizedOrigin, DispatchInfoOf, DispatchOriginOf, Dispatchable, Implication,
PostDispatchInfoOf, TransactionExtension, ValidateResult,
},
transaction_validity::{
InvalidTransaction, TransactionSource, TransactionValidityError, ValidTransaction,
},
DispatchError::BadOrigin,
DispatchResult, RuntimeDebug, SaturatedConversion, Saturating, Weight,
};
use scale_info::TypeInfo;
#[derive(Clone, Debug)]
pub struct Allowance<Balance> {
pub max: Balance,
pub recovery_per_block: Balance,
}
pub trait RestrictedEntity<OriginCaller, Balance>: Sized {
fn allowance(&self) -> Allowance<Balance>;
fn restricted_entity(caller: &OriginCaller) -> Option<Self>;
#[cfg(feature = "runtime-benchmarks")]
fn benchmarked_restricted_origin() -> OriginCaller;
}
pub use pezpallet::*;
#[pezframe_support::pezpallet]
pub mod pezpallet {
use super::*;
use pezframe_support::{pezpallet_prelude::*, traits::ContainsPair};
use pezframe_system::pezpallet_prelude::*;
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct Usage<Balance, BlockNumber> {
pub used: Balance,
pub at_block: BlockNumber,
}
pub(crate) type OriginCallerFor<T> =
<<T as pezframe_system::Config>::RuntimeOrigin as OriginTrait>::PalletsOrigin;
pub(crate) type BalanceOf<T> =
<<T as pezpallet_transaction_payment::Config>::OnChargeTransaction as OnChargeTransaction<
T,
>>::Balance;
#[pezpallet::pezpallet]
pub struct Pezpallet<T>(_);
#[pezpallet::storage]
pub type Usages<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::RestrictedEntity,
Usage<BalanceOf<T>, BlockNumberFor<T>>,
>;
#[pezpallet::config]
pub trait Config:
pezframe_system::Config<
RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
RuntimeOrigin: AsTransactionAuthorizedOrigin,
> + pezpallet_transaction_payment::Config
+ Send
+ Sync
{
type WeightInfo: WeightInfo;
type RestrictedEntity: RestrictedEntity<OriginCallerFor<Self>, BalanceOf<Self>>
+ Parameter
+ MaxEncodedLen;
type OperationAllowedOneTimeExcess: ContainsPair<Self::RestrictedEntity, Self::RuntimeCall>;
#[allow(deprecated)]
type RuntimeEvent: From<Event<Self>>
+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
}
#[pezpallet::error]
pub enum Error<T> {
NoUsage,
NotZero,
}
#[pezpallet::event]
#[pezpallet::generate_deposit(fn deposit_event)]
pub enum Event<T: Config> {
UsageCleaned { entity: T::RestrictedEntity },
}
#[pezpallet::call(weight = <T as Config>::WeightInfo)]
impl<T: Config> Pezpallet<T> {
#[pezpallet::call_index(1)]
pub fn clean_usage(
origin: OriginFor<T>,
entity: T::RestrictedEntity,
) -> DispatchResultWithPostInfo {
if ensure_none(origin.clone()).is_ok() {
return Err(BadOrigin.into());
}
let Some(mut usage) = Usages::<T>::take(&entity) else {
return Err(Error::<T>::NoUsage.into());
};
let now = pezframe_system::Pezpallet::<T>::block_number();
let elapsed = now.saturating_sub(usage.at_block).saturated_into::<u32>();
let allowance = entity.allowance();
let receive_back = allowance.recovery_per_block.saturating_mul(elapsed.into());
usage.used = usage.used.saturating_sub(receive_back);
ensure!(usage.used.is_zero(), Error::<T>::NotZero);
Self::deposit_event(Event::UsageCleaned { entity });
Ok(Pays::No.into())
}
}
}
fn extrinsic_fee<T: Config>(weight: Weight, length: usize) -> BalanceOf<T> {
let weight_fee = T::WeightToFee::weight_to_fee(&weight);
let length_fee = T::LengthToFee::weight_to_fee(&Weight::from_parts(length as u64, 0));
weight_fee.saturating_add(length_fee)
}
#[derive(
Encode, Decode, Clone, Eq, PartialEq, TypeInfo, RuntimeDebugNoBound, DecodeWithMemTracking,
)]
#[scale_info(skip_type_params(T))]
pub struct RestrictOrigin<T>(bool, core::marker::PhantomData<T>);
impl<T> RestrictOrigin<T> {
pub fn new(enable: bool) -> Self {
Self(enable, core::marker::PhantomData)
}
}
#[derive(RuntimeDebugNoBound)]
pub enum Val<T: Config> {
Charge { fee: BalanceOf<T>, entity: T::RestrictedEntity },
NoCharge,
}
pub enum Pre<T: Config> {
Charge {
fee: BalanceOf<T>,
entity: T::RestrictedEntity,
},
NoCharge {
refund: Weight,
},
}
impl<T: Config> TransactionExtension<T::RuntimeCall> for RestrictOrigin<T> {
const IDENTIFIER: &'static str = "RestrictOrigins";
type Implicit = ();
type Val = Val<T>;
type Pre = Pre<T>;
fn weight(&self, _call: &T::RuntimeCall) -> pezframe_support::weights::Weight {
if !self.0 {
return Weight::zero();
}
<T as Config>::WeightInfo::restrict_origin_tx_ext()
}
fn validate(
&self,
origin: DispatchOriginOf<T::RuntimeCall>,
call: &T::RuntimeCall,
info: &DispatchInfoOf<T::RuntimeCall>,
len: usize,
_self_implicit: (),
_inherited_implication: &impl Implication,
_source: TransactionSource,
) -> ValidateResult<Self::Val, T::RuntimeCall> {
let origin_caller = origin.caller();
let Some(entity) = T::RestrictedEntity::restricted_entity(origin_caller) else {
return Ok((ValidTransaction::default(), Val::NoCharge, origin));
};
let allowance = T::RestrictedEntity::allowance(&entity);
if !self.0 {
return Err(InvalidTransaction::Call.into());
}
let now = pezframe_system::Pezpallet::<T>::block_number();
let mut usage = match Usages::<T>::get(&entity) {
Some(mut usage) => {
let elapsed = now.saturating_sub(usage.at_block).saturated_into::<u32>();
let receive_back = allowance.recovery_per_block.saturating_mul(elapsed.into());
usage.used = usage.used.saturating_sub(receive_back);
usage.at_block = now;
usage
},
None => Usage { used: 0u32.into(), at_block: now },
};
let usage_without_new_xt = usage.used;
let fee = extrinsic_fee::<T>(info.total_weight(), len);
usage.used = usage.used.saturating_add(fee);
Usages::<T>::insert(&entity, &usage);
let allowed_one_time_excess = || {
usage_without_new_xt == 0u32.into()
&& T::OperationAllowedOneTimeExcess::contains(&entity, call)
};
if usage.used <= allowance.max || allowed_one_time_excess() {
Ok((ValidTransaction::default(), Val::Charge { fee, entity }, origin))
} else {
Err(InvalidTransaction::Payment.into())
}
}
fn prepare(
self,
val: Self::Val,
_origin: &DispatchOriginOf<T::RuntimeCall>,
call: &T::RuntimeCall,
_info: &DispatchInfoOf<T::RuntimeCall>,
_len: usize,
) -> Result<Self::Pre, TransactionValidityError> {
match val {
Val::Charge { fee, entity } => Ok(Pre::Charge { fee, entity }),
Val::NoCharge => Ok(Pre::NoCharge { refund: self.weight(call) }),
}
}
fn post_dispatch_details(
pre: Self::Pre,
_info: &DispatchInfoOf<T::RuntimeCall>,
post_info: &PostDispatchInfoOf<T::RuntimeCall>,
_len: usize,
_result: &DispatchResult,
) -> Result<Weight, TransactionValidityError> {
match pre {
Pre::Charge { fee, entity } => {
if post_info.pays_fee == Pays::No {
Usages::<T>::mutate_exists(entity, |maybe_usage| {
if let Some(usage) = maybe_usage {
usage.used = usage.used.saturating_sub(fee);
if usage.used.is_zero() {
*maybe_usage = None;
}
}
});
Ok(Weight::zero())
} else {
Ok(Weight::zero())
}
},
Pre::NoCharge { refund } => Ok(refund),
}
}
}