1#![doc = include_str!("../README.md")]
11#![allow(clippy::expect_used)]
13#![cfg_attr(not(feature = "std"), no_std)]
14
15use frame_support::{
16 dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo},
17 pallet_prelude::*,
18 traits::{IsSubType, IsType},
19 weights::{Weight, WeightToFee},
20 DefaultNoBound,
21};
22use frame_system::pallet_prelude::*;
23use pallet_transaction_payment::{FeeDetails, InclusionFee, OnChargeTransaction};
24use parity_scale_codec::{Decode, Encode};
25use scale_info::TypeInfo;
26use sp_runtime::{
27 traits::{
28 AsSystemOriginSigner, DispatchInfoOf, Dispatchable, PostDispatchInfoOf,
29 TransactionExtension, Zero,
30 },
31 transaction_validity::{
32 InvalidTransaction, TransactionSource, TransactionValidityError, ValidTransaction,
33 },
34 DispatchResult, FixedPointOperand, Saturating,
35};
36extern crate alloc;
37use alloc::{boxed::Box, vec, vec::Vec};
38use common_primitives::{
39 capacity::{Nontransferable, Replenishable},
40 msa::MsaKeyProvider,
41 node::UtilityProvider,
42};
43use core::ops::Mul;
44pub use pallet::*;
45use sp_runtime::Permill;
46pub use weights::*;
47
48mod payment;
49pub use payment::*;
50
51pub use types::GetStableWeight;
52pub mod types;
53
54pub mod capacity_stable_weights;
55
56use crate::types::GetAddKeyData;
57use capacity_stable_weights::CAPACITY_EXTRINSIC_BASE_WEIGHT;
58
59pub(crate) type OnChargeTransactionOf<T> =
61 <T as pallet_transaction_payment::Config>::OnChargeTransaction;
62
63pub(crate) type BalanceOf<T> = <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::Balance;
65
66pub(crate) type LiquidityInfoOf<T> =
68 <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::LiquidityInfo;
69
70pub(crate) type CapacityOf<T> = <T as Config>::Capacity;
72
73pub(crate) type CapacityBalanceOf<T> = <CapacityOf<T> as Nontransferable>::Balance;
75
76pub(crate) type ChargeCapacityBalanceOf<T> =
77 <<T as Config>::OnChargeCapacityTransaction as OnChargeCapacityTransaction<T>>::Balance;
78
79#[derive(Encode, Decode, DefaultNoBound, TypeInfo)]
81pub enum InitialPayment<T: Config> {
82 #[default]
84 Free,
85 Token(LiquidityInfoOf<T>),
87 Capacity,
89}
90
91#[cfg(feature = "std")]
92impl<T: Config> InitialPayment<T> {
93 pub fn is_free(&self) -> bool {
94 matches!(*self, InitialPayment::Free)
95 }
96
97 pub fn is_capacity(&self) -> bool {
98 matches!(*self, InitialPayment::Capacity)
99 }
100
101 pub fn is_token(&self) -> bool {
102 matches!(*self, InitialPayment::Token(_))
103 }
104}
105
106impl<T: Config> core::fmt::Debug for InitialPayment<T> {
107 #[cfg(feature = "std")]
108 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
109 match *self {
110 InitialPayment::Free => write!(f, "Nothing"),
111 InitialPayment::Capacity => write!(f, "Token"),
112 InitialPayment::Token(_) => write!(f, "Imbalance"),
113 }
114 }
115
116 #[cfg(not(feature = "std"))]
117 fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
118 Ok(())
119 }
120}
121
122#[cfg(test)]
123mod tests;
124
125#[cfg(feature = "runtime-benchmarks")]
126mod benchmarking;
127
128pub mod weights;
129
130#[allow(dead_code)]
131#[frame_support::pallet]
132pub mod pallet {
133 use super::*;
134 use common_primitives::msa::{MessageSourceId, MsaKeyProvider};
135
136 #[pallet::pallet]
139 pub struct Pallet<T>(_);
140
141 #[pallet::config]
142 pub trait Config: frame_system::Config + pallet_transaction_payment::Config {
143 #[allow(deprecated)]
145 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
146
147 type RuntimeCall: Parameter
149 + Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
150 + GetDispatchInfo
151 + From<frame_system::Call<Self>>
152 + IsSubType<Call<Self>>
153 + IsType<<Self as frame_system::Config>::RuntimeCall>;
154
155 type Capacity: Replenishable + Nontransferable;
157
158 type WeightInfo: WeightInfo;
160
161 type CapacityCalls: GetStableWeight<<Self as Config>::RuntimeCall, Weight>;
163
164 type OnChargeCapacityTransaction: OnChargeCapacityTransaction<Self>;
166
167 #[pallet::constant]
169 type MaximumCapacityBatchLength: Get<u8>;
170
171 type BatchProvider: UtilityProvider<OriginFor<Self>, <Self as Config>::RuntimeCall>;
172
173 type MsaKeyProvider: MsaKeyProvider<AccountId = Self::AccountId>;
174 type MsaCallFilter: GetAddKeyData<
175 <Self as Config>::RuntimeCall,
176 Self::AccountId,
177 MessageSourceId,
178 >;
179 }
180
181 #[pallet::event]
182 #[pallet::generate_deposit(pub (super) fn deposit_event)]
183 pub enum Event<T: Config> {}
184
185 #[pallet::error]
186 pub enum Error<T> {
187 BatchedCallAmountExceedsMaximum,
189 }
190
191 #[pallet::call]
192 impl<T: Config> Pallet<T> {
193 #[pallet::call_index(0)]
196 #[pallet::weight({
197 let dispatch_info = call.get_dispatch_info();
198 (< T as Config >::WeightInfo::pay_with_capacity().saturating_add(dispatch_info.call_weight), dispatch_info.class)
199 })]
200 #[allow(clippy::useless_conversion)]
201 pub fn pay_with_capacity(
202 origin: OriginFor<T>,
203 call: Box<<T as Config>::RuntimeCall>,
204 ) -> DispatchResultWithPostInfo {
205 ensure_signed(origin.clone())?;
206
207 call.dispatch(origin)
208 }
209
210 #[pallet::call_index(1)]
213 #[pallet::weight({
214 let dispatch_infos = calls.iter().map(|call| call.get_dispatch_info()).collect::<Vec<_>>();
215 let dispatch_weight = dispatch_infos.iter()
216 .map(|di| di.call_weight)
217 .fold(Weight::zero(), |total: Weight, weight: Weight| total.saturating_add(weight));
218 (< T as Config >::WeightInfo::pay_with_capacity_batch_all(calls.len() as u32).saturating_add(dispatch_weight), DispatchClass::Normal)
219 })]
220 #[allow(clippy::useless_conversion)]
221 pub fn pay_with_capacity_batch_all(
222 origin: OriginFor<T>,
223 calls: Vec<<T as Config>::RuntimeCall>,
224 ) -> DispatchResultWithPostInfo {
225 ensure_signed(origin.clone())?;
226 ensure!(
227 calls.len() <= T::MaximumCapacityBatchLength::get().into(),
228 Error::<T>::BatchedCallAmountExceedsMaximum
229 );
230
231 T::BatchProvider::batch_all(origin, calls)
232 }
233 }
234}
235
236impl<T: Config> Pallet<T> {
237 pub fn get_capacity_overhead_weight() -> Weight {
246 T::DbWeight::get().reads(2).saturating_add(T::DbWeight::get().writes(1))
247 }
248
249 pub fn compute_capacity_fee(len: u32, extrinsic_weight: Weight) -> BalanceOf<T> {
257 let weight_fee = Self::weight_to_fee(extrinsic_weight);
258
259 let len_fee = Self::length_to_fee(len);
260 let base_fee = Self::weight_to_fee(CAPACITY_EXTRINSIC_BASE_WEIGHT);
261
262 base_fee.saturating_add(weight_fee).saturating_add(len_fee)
263 }
264
265 pub fn compute_capacity_fee_details(
274 runtime_call: &<T as Config>::RuntimeCall,
275 dispatch_weight: &Weight,
276 len: u32,
277 ) -> FeeDetails<BalanceOf<T>> {
278 let calls = T::CapacityCalls::get_inner_calls(runtime_call)
279 .expect("A collection of calls is expected at minimum one.");
280
281 let mut calls_weight_sum = Weight::zero();
282 for inner_call in calls {
283 let call_weight = T::CapacityCalls::get_stable_weight(inner_call).unwrap_or_default();
284 calls_weight_sum = calls_weight_sum.saturating_add(call_weight);
285 }
286
287 let mut fees = FeeDetails { inclusion_fee: None, tip: Zero::zero() };
288 if !calls_weight_sum.is_zero() {
289 if let Some(weight) = calls_weight_sum.checked_add(dispatch_weight) {
290 let weight_fee = Self::weight_to_fee(weight);
291 let len_fee = Self::length_to_fee(len);
292 let base_fee = Self::weight_to_fee(CAPACITY_EXTRINSIC_BASE_WEIGHT);
293
294 let tip = Zero::zero();
295 fees = FeeDetails {
296 inclusion_fee: Some(InclusionFee {
297 base_fee,
298 len_fee,
299 adjusted_weight_fee: weight_fee,
300 }),
301 tip,
302 };
303 }
304 }
305 fees
306 }
307 pub fn length_to_fee(length: u32) -> BalanceOf<T> {
309 T::LengthToFee::weight_to_fee(&Weight::from_parts(length as u64, 0))
310 }
311
312 pub fn weight_to_fee(weight: Weight) -> BalanceOf<T> {
315 let capped_weight = weight.min(T::BlockWeights::get().max_block);
318 T::WeightToFee::weight_to_fee(&capped_weight)
319 }
320}
321
322pub enum ChargeFrqTransactionPaymentError {
324 CallIsNotCapacityEligible,
326 InvalidMsaKey,
328 TargetCapacityNotFound,
330 BelowMinDeposit,
332}
333
334#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)]
345#[scale_info(skip_type_params(T))]
346pub struct ChargeFrqTransactionPayment<T: Config>(#[codec(compact)] BalanceOf<T>);
347
348impl ChargeFrqTransactionPaymentError {
349 pub fn into(self) -> TransactionValidityError {
350 TransactionValidityError::from(InvalidTransaction::Custom(self as u8))
351 }
352}
353
354impl<T: Config> ChargeFrqTransactionPayment<T>
355where
356 BalanceOf<T>: Send + Sync + FixedPointOperand + IsType<ChargeCapacityBalanceOf<T>>,
357 <T as frame_system::Config>::RuntimeCall:
358 Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo> + IsSubType<Call<T>>,
359{
360 pub fn from(tip: BalanceOf<T>) -> Self {
362 Self(tip)
363 }
364
365 pub fn tip(&self, call: &<T as frame_system::Config>::RuntimeCall) -> BalanceOf<T> {
367 match call.is_sub_type() {
368 Some(Call::pay_with_capacity { .. }) |
369 Some(Call::pay_with_capacity_batch_all { .. }) => Zero::zero(),
370 _ => self.0,
371 }
372 }
373
374 fn dryrun_withdraw_fee(
376 &self,
377 who: &T::AccountId,
378 call: &<T as frame_system::Config>::RuntimeCall,
379 info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
380 len: usize,
381 ) -> Result<BalanceOf<T>, TransactionValidityError> {
382 match call.is_sub_type() {
383 Some(Call::pay_with_capacity { call }) =>
384 self.dryrun_withdraw_capacity_fee(who, &vec![*call.clone()], len),
385
386 Some(Call::pay_with_capacity_batch_all { calls }) =>
387 self.dryrun_withdraw_capacity_fee(who, calls, len),
388
389 _ => self.dryrun_withdraw_token_fee(who, call, info, len, self.tip(call)),
390 }
391 }
392
393 fn dryrun_withdraw_capacity_fee(
394 &self,
395 who: &T::AccountId,
396 calls: &Vec<<T as Config>::RuntimeCall>,
397 len: usize,
398 ) -> Result<BalanceOf<T>, TransactionValidityError> {
399 let mut calls_weight_sum = Weight::zero();
400 for call in calls {
401 let call_weight = T::CapacityCalls::get_stable_weight(call)
402 .ok_or(ChargeFrqTransactionPaymentError::CallIsNotCapacityEligible.into())?;
403 calls_weight_sum = calls_weight_sum.saturating_add(call_weight);
404 }
405 let fee = Pallet::<T>::compute_capacity_fee(len as u32, calls_weight_sum);
406 T::OnChargeCapacityTransaction::can_withdraw_fee(who, fee.into())?;
407 Ok(fee)
408 }
409
410 fn dryrun_withdraw_token_fee(
411 &self,
412 who: &T::AccountId,
413 call: &<T as frame_system::Config>::RuntimeCall,
414 info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
415 len: usize,
416 tip: BalanceOf<T>,
417 ) -> Result<BalanceOf<T>, TransactionValidityError> {
418 let fee = pallet_transaction_payment::Pallet::<T>::compute_fee(len as u32, info, tip);
419 if fee.is_zero() {
420 return Ok(Default::default());
421 }
422 <<T as pallet_transaction_payment::Config>::OnChargeTransaction as OnChargeTransaction<
423 T,
424 >>::can_withdraw_fee(who, call, info, fee, tip)?;
425 Ok(fee)
426 }
427
428 fn withdraw_fee(
430 &self,
431 who: &T::AccountId,
432 call: &<T as frame_system::Config>::RuntimeCall,
433 info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
434 len: usize,
435 ) -> Result<(BalanceOf<T>, InitialPayment<T>), TransactionValidityError> {
436 match call.is_sub_type() {
437 Some(Call::pay_with_capacity { call }) =>
438 self.withdraw_capacity_fee(who, &vec![*call.clone()], len),
439 Some(Call::pay_with_capacity_batch_all { calls }) =>
440 self.withdraw_capacity_fee(who, calls, len),
441 _ => self.withdraw_token_fee(who, call, info, len, self.tip(call)),
442 }
443 }
444
445 fn withdraw_capacity_fee(
447 &self,
448 key: &T::AccountId,
449 calls: &Vec<<T as Config>::RuntimeCall>,
450 len: usize,
451 ) -> Result<(BalanceOf<T>, InitialPayment<T>), TransactionValidityError> {
452 let mut calls_weight_sum = Weight::zero();
453 let mut subsidized_calls_weight_sum = Weight::zero();
454
455 for call in calls {
456 let call_weight = T::CapacityCalls::get_stable_weight(call)
457 .ok_or(ChargeFrqTransactionPaymentError::CallIsNotCapacityEligible.into())?;
458 calls_weight_sum = calls_weight_sum.saturating_add(call_weight);
459
460 if self.call_is_adding_eligible_key_to_msa(call) {
461 subsidized_calls_weight_sum =
462 subsidized_calls_weight_sum.saturating_add(call_weight);
463 }
464 }
465 let capacity_fee = Pallet::<T>::compute_capacity_fee(len as u32, calls_weight_sum)
466 .saturating_sub(Self::subsidized_calls_reduction(len, subsidized_calls_weight_sum));
467 let fee = T::OnChargeCapacityTransaction::withdraw_fee(key, capacity_fee.into())?;
468
469 Ok((fee.into(), InitialPayment::Capacity))
470 }
471
472 fn subsidized_calls_reduction(len: usize, eligible_call_weight: Weight) -> BalanceOf<T> {
474 if eligible_call_weight.is_zero() {
475 0u32.into()
476 } else {
477 let reduction: Permill = Permill::from_percent(70u32);
478 reduction.mul(Pallet::<T>::compute_capacity_fee(len as u32, eligible_call_weight))
479 }
480 }
481
482 fn call_is_adding_eligible_key_to_msa(&self, call: &<T as Config>::RuntimeCall) -> bool {
483 if let Some((owner_account_id, new_account_id, msa_id)) =
484 T::MsaCallFilter::get_add_key_data(call)
485 {
486 return T::MsaKeyProvider::key_eligible_for_subsidized_addition(
487 owner_account_id,
488 new_account_id,
489 msa_id,
490 );
491 }
492 false
493 }
494
495 fn withdraw_token_fee(
497 &self,
498 who: &T::AccountId,
499 call: &<T as frame_system::Config>::RuntimeCall,
500 info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
501 len: usize,
502 tip: BalanceOf<T>,
503 ) -> Result<(BalanceOf<T>, InitialPayment<T>), TransactionValidityError> {
504 let fee = pallet_transaction_payment::Pallet::<T>::compute_fee(len as u32, info, tip);
505 if fee.is_zero() {
506 return Ok((fee, InitialPayment::Free));
507 }
508
509 <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::withdraw_fee(
510 who, call, info, fee, tip,
511 )
512 .map(|i| (fee, InitialPayment::Token(i)))
513 .map_err(|_| -> TransactionValidityError { InvalidTransaction::Payment.into() })
514 }
515}
516
517impl<T: Config> core::fmt::Debug for ChargeFrqTransactionPayment<T> {
518 #[cfg(feature = "std")]
519 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
520 write!(f, "ChargeFrqTransactionPayment<{:?}>", self.0)
521 }
522 #[cfg(not(feature = "std"))]
523 fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
524 Ok(())
525 }
526}
527
528#[derive(RuntimeDebugNoBound)]
530pub enum Val<T: Config> {
531 Charge { tip: BalanceOf<T>, who: T::AccountId, fee: BalanceOf<T> },
532 NoCharge,
533}
534
535#[derive(RuntimeDebugNoBound)]
537pub enum Pre<T: Config> {
538 Charge {
539 tip: BalanceOf<T>,
540 who: T::AccountId,
541 initial_payment: InitialPayment<T>,
542 weight: Weight,
543 },
544 NoCharge { refund: Weight },
546}
547
548impl<T: Config> TransactionExtension<<T as frame_system::Config>::RuntimeCall>
549 for ChargeFrqTransactionPayment<T>
550where
551 <T as frame_system::Config>::RuntimeCall:
552 IsSubType<Call<T>> + Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
553 BalanceOf<T>: Send
554 + Sync
555 + FixedPointOperand
556 + From<u64>
557 + IsType<ChargeCapacityBalanceOf<T>>
558 + IsType<CapacityBalanceOf<T>>,
559 <T as frame_system::Config>::RuntimeOrigin: AsSystemOriginSigner<T::AccountId> + Clone,
560{
561 const IDENTIFIER: &'static str = "ChargeTransactionPayment";
562 type Implicit = ();
563 type Val = Val<T>;
564 type Pre = Pre<T>;
565
566 fn weight(&self, call: &<T as frame_system::Config>::RuntimeCall) -> Weight {
567 match call.is_sub_type() {
568 Some(Call::pay_with_capacity { .. }) |
569 Some(Call::pay_with_capacity_batch_all { .. }) =>
570 <T as Config>::WeightInfo::charge_tx_payment_capacity_based(),
571 _ => {
572 let info = call.get_dispatch_info();
574 if info.pays_fee == Pays::No {
575 <T as Config>::WeightInfo::charge_tx_payment_free()
576 } else {
577 <T as Config>::WeightInfo::charge_tx_payment_token_based()
578 }
579 },
580 }
581 }
582
583 fn validate(
584 &self,
585 origin: <T as frame_system::Config>::RuntimeOrigin,
586 call: &<T as frame_system::Config>::RuntimeCall,
587 info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
588 len: usize,
589 _self_implicit: Self::Implicit,
590 _inherited_implication: &impl Encode,
591 _source: TransactionSource,
592 ) -> sp_runtime::traits::ValidateResult<Self::Val, <T as frame_system::Config>::RuntimeCall> {
593 let Some(who) = origin.as_system_origin_signer() else {
594 return Ok((
595 sp_runtime::transaction_validity::ValidTransaction::default(),
596 Val::NoCharge,
597 origin,
598 ));
599 };
600 let fee = self.dryrun_withdraw_fee(who, call, info, len)?;
601 let tip = self.tip(call);
602 let priority = pallet_transaction_payment::ChargeTransactionPayment::<T>::get_priority(
603 info, len, tip, fee,
604 );
605 let val = Val::Charge { tip, who: who.clone(), fee };
606 let validity = ValidTransaction { priority, ..Default::default() };
607 Ok((validity, val, origin))
608 }
609
610 fn prepare(
611 self,
612 val: Self::Val,
613 _origin: &<T as frame_system::Config>::RuntimeOrigin,
614 call: &<T as frame_system::Config>::RuntimeCall,
615 info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
616 len: usize,
617 ) -> Result<Self::Pre, TransactionValidityError> {
618 match val {
619 Val::Charge { tip, who, .. } => {
620 let (_fee, initial_payment) = self.withdraw_fee(&who, call, info, len)?;
621 Ok(Pre::Charge { tip, who, initial_payment, weight: self.weight(call) })
622 },
623 Val::NoCharge => Ok(Pre::NoCharge { refund: self.weight(call) }),
624 }
625 }
626
627 fn post_dispatch_details(
628 pre: Self::Pre,
629 info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
630 post_info: &PostDispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
631 len: usize,
632 result: &DispatchResult,
633 ) -> Result<Weight, TransactionValidityError> {
634 match pre {
635 Pre::Charge { tip, who, initial_payment, .. } => match initial_payment {
636 InitialPayment::Token(already_withdrawn) => {
637 let weight = pallet_transaction_payment::ChargeTransactionPayment::<T>::post_dispatch_details(
641 pallet_transaction_payment::Pre::Charge { tip, who, imbalance: already_withdrawn },
642 info,
643 post_info,
644 len,
645 result,
646 )?;
647 Ok(weight)
648 },
649 InitialPayment::Capacity => {
650 debug_assert!(tip.is_zero(), "tip should be zero for Capacity tx.");
651 Ok(Weight::zero())
652 },
653 InitialPayment::Free => {
654 debug_assert!(tip.is_zero(), "tip should be zero if initial fee was zero.");
655 Ok(Weight::zero())
656 },
657 },
658 Pre::NoCharge { refund } => Ok(refund),
659 }
660 }
661}