pezpallet_origin_restriction/
lib.rs1#![cfg_attr(not(feature = "std"), no_std)]
42
43#[cfg(feature = "runtime-benchmarks")]
44mod benchmarking;
45#[cfg(test)]
46mod mock;
47#[cfg(test)]
48mod tests;
49pub mod weights;
50
51extern crate alloc;
52
53pub use weights::WeightInfo;
54
55use codec::{Decode, DecodeWithMemTracking, Encode};
56use pezframe_support::{
57 dispatch::{DispatchInfo, PostDispatchInfo},
58 pezpallet_prelude::{Pays, Zero},
59 traits::{ContainsPair, OriginTrait},
60 weights::WeightToFee,
61 Parameter, RuntimeDebugNoBound,
62};
63use pezframe_system::pezpallet_prelude::BlockNumberFor;
64use pezpallet_transaction_payment::OnChargeTransaction;
65use pezsp_runtime::{
66 traits::{
67 AsTransactionAuthorizedOrigin, DispatchInfoOf, DispatchOriginOf, Dispatchable, Implication,
68 PostDispatchInfoOf, TransactionExtension, ValidateResult,
69 },
70 transaction_validity::{
71 InvalidTransaction, TransactionSource, TransactionValidityError, ValidTransaction,
72 },
73 DispatchError::BadOrigin,
74 DispatchResult, RuntimeDebug, SaturatedConversion, Saturating, Weight,
75};
76use scale_info::TypeInfo;
77
78#[derive(Clone, Debug)]
80pub struct Allowance<Balance> {
81 pub max: Balance,
83 pub recovery_per_block: Balance,
85}
86
87pub trait RestrictedEntity<OriginCaller, Balance>: Sized {
89 fn allowance(&self) -> Allowance<Balance>;
91 fn restricted_entity(caller: &OriginCaller) -> Option<Self>;
93
94 #[cfg(feature = "runtime-benchmarks")]
95 fn benchmarked_restricted_origin() -> OriginCaller;
96}
97
98pub use pezpallet::*;
99#[pezframe_support::pezpallet]
100pub mod pezpallet {
101 use super::*;
102 use pezframe_support::{pezpallet_prelude::*, traits::ContainsPair};
103 use pezframe_system::pezpallet_prelude::*;
104
105 #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
107 pub struct Usage<Balance, BlockNumber> {
108 pub used: Balance,
110 pub at_block: BlockNumber,
112 }
113
114 pub(crate) type OriginCallerFor<T> =
115 <<T as pezframe_system::Config>::RuntimeOrigin as OriginTrait>::PalletsOrigin;
116 pub(crate) type BalanceOf<T> =
117 <<T as pezpallet_transaction_payment::Config>::OnChargeTransaction as OnChargeTransaction<
118 T,
119 >>::Balance;
120
121 #[pezpallet::pezpallet]
122 pub struct Pezpallet<T>(_);
123
124 #[pezpallet::storage]
126 pub type Usages<T: Config> = StorageMap<
127 _,
128 Blake2_128Concat,
129 T::RestrictedEntity,
130 Usage<BalanceOf<T>, BlockNumberFor<T>>,
131 >;
132
133 #[pezpallet::config]
134 pub trait Config:
135 pezframe_system::Config<
136 RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
137 RuntimeOrigin: AsTransactionAuthorizedOrigin,
138 > + pezpallet_transaction_payment::Config
139 + Send
140 + Sync
141 {
142 type WeightInfo: WeightInfo;
144
145 type RestrictedEntity: RestrictedEntity<OriginCallerFor<Self>, BalanceOf<Self>>
155 + Parameter
156 + MaxEncodedLen;
157
158 type OperationAllowedOneTimeExcess: ContainsPair<Self::RestrictedEntity, Self::RuntimeCall>;
162
163 #[allow(deprecated)]
165 type RuntimeEvent: From<Event<Self>>
166 + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
167 }
168
169 #[pezpallet::error]
170 pub enum Error<T> {
171 NoUsage,
173 NotZero,
175 }
176
177 #[pezpallet::event]
178 #[pezpallet::generate_deposit(fn deposit_event)]
179 pub enum Event<T: Config> {
180 UsageCleaned { entity: T::RestrictedEntity },
182 }
183
184 #[pezpallet::call(weight = <T as Config>::WeightInfo)]
185 impl<T: Config> Pezpallet<T> {
186 #[pezpallet::call_index(1)]
190 pub fn clean_usage(
191 origin: OriginFor<T>,
192 entity: T::RestrictedEntity,
193 ) -> DispatchResultWithPostInfo {
194 if ensure_none(origin.clone()).is_ok() {
197 return Err(BadOrigin.into());
198 }
199
200 let Some(mut usage) = Usages::<T>::take(&entity) else {
201 return Err(Error::<T>::NoUsage.into());
202 };
203
204 let now = pezframe_system::Pezpallet::<T>::block_number();
205 let elapsed = now.saturating_sub(usage.at_block).saturated_into::<u32>();
206
207 let allowance = entity.allowance();
208 let receive_back = allowance.recovery_per_block.saturating_mul(elapsed.into());
209 usage.used = usage.used.saturating_sub(receive_back);
210
211 ensure!(usage.used.is_zero(), Error::<T>::NotZero);
212
213 Self::deposit_event(Event::UsageCleaned { entity });
214
215 Ok(Pays::No.into())
216 }
217 }
218}
219
220fn extrinsic_fee<T: Config>(weight: Weight, length: usize) -> BalanceOf<T> {
221 let weight_fee = T::WeightToFee::weight_to_fee(&weight);
222 let length_fee = T::LengthToFee::weight_to_fee(&Weight::from_parts(length as u64, 0));
223 weight_fee.saturating_add(length_fee)
224}
225
226#[derive(
233 Encode, Decode, Clone, Eq, PartialEq, TypeInfo, RuntimeDebugNoBound, DecodeWithMemTracking,
234)]
235#[scale_info(skip_type_params(T))]
236pub struct RestrictOrigin<T>(bool, core::marker::PhantomData<T>);
237
238impl<T> RestrictOrigin<T> {
239 pub fn new(enable: bool) -> Self {
241 Self(enable, core::marker::PhantomData)
242 }
243}
244
245#[derive(RuntimeDebugNoBound)]
247pub enum Val<T: Config> {
248 Charge { fee: BalanceOf<T>, entity: T::RestrictedEntity },
249 NoCharge,
250}
251
252pub enum Pre<T: Config> {
255 Charge {
256 fee: BalanceOf<T>,
257 entity: T::RestrictedEntity,
258 },
259 NoCharge {
260 refund: Weight,
262 },
263}
264
265impl<T: Config> TransactionExtension<T::RuntimeCall> for RestrictOrigin<T> {
266 const IDENTIFIER: &'static str = "RestrictOrigins";
267 type Implicit = ();
268 type Val = Val<T>;
269 type Pre = Pre<T>;
270
271 fn weight(&self, _call: &T::RuntimeCall) -> pezframe_support::weights::Weight {
272 if !self.0 {
273 return Weight::zero();
274 }
275
276 <T as Config>::WeightInfo::restrict_origin_tx_ext()
277 }
278
279 fn validate(
280 &self,
281 origin: DispatchOriginOf<T::RuntimeCall>,
282 call: &T::RuntimeCall,
283 info: &DispatchInfoOf<T::RuntimeCall>,
284 len: usize,
285 _self_implicit: (),
286 _inherited_implication: &impl Implication,
287 _source: TransactionSource,
288 ) -> ValidateResult<Self::Val, T::RuntimeCall> {
289 let origin_caller = origin.caller();
290 let Some(entity) = T::RestrictedEntity::restricted_entity(origin_caller) else {
291 return Ok((ValidTransaction::default(), Val::NoCharge, origin));
292 };
293 let allowance = T::RestrictedEntity::allowance(&entity);
294
295 if !self.0 {
296 return Err(InvalidTransaction::Call.into());
299 }
300
301 let now = pezframe_system::Pezpallet::<T>::block_number();
302 let mut usage = match Usages::<T>::get(&entity) {
303 Some(mut usage) => {
304 let elapsed = now.saturating_sub(usage.at_block).saturated_into::<u32>();
305 let receive_back = allowance.recovery_per_block.saturating_mul(elapsed.into());
306 usage.used = usage.used.saturating_sub(receive_back);
307 usage.at_block = now;
308 usage
309 },
310 None => Usage { used: 0u32.into(), at_block: now },
311 };
312
313 let usage_without_new_xt = usage.used;
315 let fee = extrinsic_fee::<T>(info.total_weight(), len);
316 usage.used = usage.used.saturating_add(fee);
317
318 Usages::<T>::insert(&entity, &usage);
319
320 let allowed_one_time_excess = || {
321 usage_without_new_xt == 0u32.into()
322 && T::OperationAllowedOneTimeExcess::contains(&entity, call)
323 };
324 if usage.used <= allowance.max || allowed_one_time_excess() {
325 Ok((ValidTransaction::default(), Val::Charge { fee, entity }, origin))
326 } else {
327 Err(InvalidTransaction::Payment.into())
328 }
329 }
330
331 fn prepare(
332 self,
333 val: Self::Val,
334 _origin: &DispatchOriginOf<T::RuntimeCall>,
335 call: &T::RuntimeCall,
336 _info: &DispatchInfoOf<T::RuntimeCall>,
337 _len: usize,
338 ) -> Result<Self::Pre, TransactionValidityError> {
339 match val {
340 Val::Charge { fee, entity } => Ok(Pre::Charge { fee, entity }),
341 Val::NoCharge => Ok(Pre::NoCharge { refund: self.weight(call) }),
342 }
343 }
344
345 fn post_dispatch_details(
346 pre: Self::Pre,
347 _info: &DispatchInfoOf<T::RuntimeCall>,
348 post_info: &PostDispatchInfoOf<T::RuntimeCall>,
349 _len: usize,
350 _result: &DispatchResult,
351 ) -> Result<Weight, TransactionValidityError> {
352 match pre {
353 Pre::Charge { fee, entity } => {
354 if post_info.pays_fee == Pays::No {
355 Usages::<T>::mutate_exists(entity, |maybe_usage| {
356 if let Some(usage) = maybe_usage {
357 usage.used = usage.used.saturating_sub(fee);
358
359 if usage.used.is_zero() {
360 *maybe_usage = None;
361 }
362 }
363 });
364 Ok(Weight::zero())
365 } else {
366 Ok(Weight::zero())
367 }
368 },
369 Pre::NoCharge { refund } => Ok(refund),
370 }
371 }
372}