#![doc = include_str!("../README.md")]
#![allow(clippy::expect_used)]
#![cfg_attr(not(feature = "std"), no_std)]
use frame_support::{
dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo},
pallet_prelude::*,
traits::{IsSubType, IsType},
weights::{Weight, WeightToFee},
DefaultNoBound,
};
use frame_system::pallet_prelude::*;
use pallet_transaction_payment::{FeeDetails, InclusionFee, OnChargeTransaction};
use parity_scale_codec::{Decode, Encode};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{
AsSystemOriginSigner, DispatchInfoOf, Dispatchable, PostDispatchInfoOf,
TransactionExtension, Zero,
},
transaction_validity::{
InvalidTransaction, TransactionSource, TransactionValidityError, ValidTransaction,
},
DispatchResult, FixedPointOperand, Saturating,
};
extern crate alloc;
use alloc::{boxed::Box, vec, vec::Vec};
use common_primitives::{
capacity::{Nontransferable, Replenishable},
msa::MsaKeyProvider,
node::UtilityProvider,
};
use core::ops::Mul;
pub use pallet::*;
use sp_runtime::Permill;
pub use weights::*;
mod payment;
pub use payment::*;
pub use types::GetStableWeight;
pub mod types;
pub mod capacity_stable_weights;
use crate::types::GetAddKeyData;
use capacity_stable_weights::CAPACITY_EXTRINSIC_BASE_WEIGHT;
pub(crate) type OnChargeTransactionOf<T> =
<T as pallet_transaction_payment::Config>::OnChargeTransaction;
pub(crate) type BalanceOf<T> = <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::Balance;
pub(crate) type LiquidityInfoOf<T> =
<OnChargeTransactionOf<T> as OnChargeTransaction<T>>::LiquidityInfo;
pub(crate) type CapacityOf<T> = <T as Config>::Capacity;
pub(crate) type CapacityBalanceOf<T> = <CapacityOf<T> as Nontransferable>::Balance;
pub(crate) type ChargeCapacityBalanceOf<T> =
<<T as Config>::OnChargeCapacityTransaction as OnChargeCapacityTransaction<T>>::Balance;
#[derive(Encode, Decode, DefaultNoBound, TypeInfo)]
pub enum InitialPayment<T: Config> {
#[default]
Free,
Token(LiquidityInfoOf<T>),
Capacity,
}
#[cfg(feature = "std")]
impl<T: Config> InitialPayment<T> {
pub fn is_free(&self) -> bool {
matches!(*self, InitialPayment::Free)
}
pub fn is_capacity(&self) -> bool {
matches!(*self, InitialPayment::Capacity)
}
pub fn is_token(&self) -> bool {
matches!(*self, InitialPayment::Token(_))
}
}
impl<T: Config> core::fmt::Debug for InitialPayment<T> {
#[cfg(feature = "std")]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match *self {
InitialPayment::Free => write!(f, "Nothing"),
InitialPayment::Capacity => write!(f, "Token"),
InitialPayment::Token(_) => write!(f, "Imbalance"),
}
}
#[cfg(not(feature = "std"))]
fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
Ok(())
}
}
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
#[allow(dead_code)]
#[frame_support::pallet]
pub mod pallet {
use super::*;
use common_primitives::msa::{MessageSourceId, MsaKeyProvider};
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config + pallet_transaction_payment::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type RuntimeCall: Parameter
+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
+ GetDispatchInfo
+ From<frame_system::Call<Self>>
+ IsSubType<Call<Self>>
+ IsType<<Self as frame_system::Config>::RuntimeCall>;
type Capacity: Replenishable + Nontransferable;
type WeightInfo: WeightInfo;
type CapacityCalls: GetStableWeight<<Self as Config>::RuntimeCall, Weight>;
type OnChargeCapacityTransaction: OnChargeCapacityTransaction<Self>;
#[pallet::constant]
type MaximumCapacityBatchLength: Get<u8>;
type BatchProvider: UtilityProvider<OriginFor<Self>, <Self as Config>::RuntimeCall>;
type MsaKeyProvider: MsaKeyProvider<AccountId = Self::AccountId>;
type MsaCallFilter: GetAddKeyData<
<Self as Config>::RuntimeCall,
Self::AccountId,
MessageSourceId,
>;
}
#[pallet::event]
#[pallet::generate_deposit(pub (super) fn deposit_event)]
pub enum Event<T: Config> {}
#[pallet::error]
pub enum Error<T> {
BatchedCallAmountExceedsMaximum,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight({
let dispatch_info = call.get_dispatch_info();
(< T as Config >::WeightInfo::pay_with_capacity().saturating_add(dispatch_info.call_weight), dispatch_info.class)
})]
pub fn pay_with_capacity(
origin: OriginFor<T>,
call: Box<<T as Config>::RuntimeCall>,
) -> DispatchResultWithPostInfo {
ensure_signed(origin.clone())?;
call.dispatch(origin)
}
#[pallet::call_index(1)]
#[pallet::weight({
let dispatch_infos = calls.iter().map(|call| call.get_dispatch_info()).collect::<Vec<_>>();
let dispatch_weight = dispatch_infos.iter()
.map(|di| di.call_weight)
.fold(Weight::zero(), |total: Weight, weight: Weight| total.saturating_add(weight));
(< T as Config >::WeightInfo::pay_with_capacity_batch_all(calls.len() as u32).saturating_add(dispatch_weight), DispatchClass::Normal)
})]
pub fn pay_with_capacity_batch_all(
origin: OriginFor<T>,
calls: Vec<<T as Config>::RuntimeCall>,
) -> DispatchResultWithPostInfo {
ensure_signed(origin.clone())?;
ensure!(
calls.len() <= T::MaximumCapacityBatchLength::get().into(),
Error::<T>::BatchedCallAmountExceedsMaximum
);
T::BatchProvider::batch_all(origin, calls)
}
}
}
impl<T: Config> Pallet<T> {
pub fn get_capacity_overhead_weight() -> Weight {
T::DbWeight::get().reads(2).saturating_add(T::DbWeight::get().writes(1))
}
pub fn compute_capacity_fee(len: u32, extrinsic_weight: Weight) -> BalanceOf<T> {
let weight_fee = Self::weight_to_fee(extrinsic_weight);
let len_fee = Self::length_to_fee(len);
let base_fee = Self::weight_to_fee(CAPACITY_EXTRINSIC_BASE_WEIGHT);
base_fee.saturating_add(weight_fee).saturating_add(len_fee)
}
pub fn compute_capacity_fee_details(
runtime_call: &<T as Config>::RuntimeCall,
dispatch_weight: &Weight,
len: u32,
) -> FeeDetails<BalanceOf<T>> {
let calls = T::CapacityCalls::get_inner_calls(runtime_call)
.expect("A collection of calls is expected at minimum one.");
let mut calls_weight_sum = Weight::zero();
for inner_call in calls {
let call_weight = T::CapacityCalls::get_stable_weight(inner_call).unwrap_or_default();
calls_weight_sum = calls_weight_sum.saturating_add(call_weight);
}
let mut fees = FeeDetails { inclusion_fee: None, tip: Zero::zero() };
if !calls_weight_sum.is_zero() {
if let Some(weight) = calls_weight_sum.checked_add(dispatch_weight) {
let weight_fee = Self::weight_to_fee(weight);
let len_fee = Self::length_to_fee(len);
let base_fee = Self::weight_to_fee(CAPACITY_EXTRINSIC_BASE_WEIGHT);
let tip = Zero::zero();
fees = FeeDetails {
inclusion_fee: Some(InclusionFee {
base_fee,
len_fee,
adjusted_weight_fee: weight_fee,
}),
tip,
};
}
}
fees
}
pub fn length_to_fee(length: u32) -> BalanceOf<T> {
T::LengthToFee::weight_to_fee(&Weight::from_parts(length as u64, 0))
}
pub fn weight_to_fee(weight: Weight) -> BalanceOf<T> {
let capped_weight = weight.min(T::BlockWeights::get().max_block);
T::WeightToFee::weight_to_fee(&capped_weight)
}
}
pub enum ChargeFrqTransactionPaymentError {
CallIsNotCapacityEligible,
InvalidMsaKey,
TargetCapacityNotFound,
BelowMinDeposit,
}
#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct ChargeFrqTransactionPayment<T: Config>(#[codec(compact)] BalanceOf<T>);
impl ChargeFrqTransactionPaymentError {
pub fn into(self) -> TransactionValidityError {
TransactionValidityError::from(InvalidTransaction::Custom(self as u8))
}
}
impl<T: Config> ChargeFrqTransactionPayment<T>
where
BalanceOf<T>: Send + Sync + FixedPointOperand + IsType<ChargeCapacityBalanceOf<T>>,
<T as frame_system::Config>::RuntimeCall:
Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo> + IsSubType<Call<T>>,
{
pub fn from(tip: BalanceOf<T>) -> Self {
Self(tip)
}
pub fn tip(&self, call: &<T as frame_system::Config>::RuntimeCall) -> BalanceOf<T> {
match call.is_sub_type() {
Some(Call::pay_with_capacity { .. }) |
Some(Call::pay_with_capacity_batch_all { .. }) => Zero::zero(),
_ => self.0,
}
}
fn dryrun_withdraw_fee(
&self,
who: &T::AccountId,
call: &<T as frame_system::Config>::RuntimeCall,
info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
len: usize,
) -> Result<BalanceOf<T>, TransactionValidityError> {
match call.is_sub_type() {
Some(Call::pay_with_capacity { call }) =>
self.dryrun_withdraw_capacity_fee(who, &vec![*call.clone()], len),
Some(Call::pay_with_capacity_batch_all { calls }) =>
self.dryrun_withdraw_capacity_fee(who, calls, len),
_ => self.dryrun_withdraw_token_fee(who, call, info, len, self.tip(call)),
}
}
fn dryrun_withdraw_capacity_fee(
&self,
who: &T::AccountId,
calls: &Vec<<T as Config>::RuntimeCall>,
len: usize,
) -> Result<BalanceOf<T>, TransactionValidityError> {
let mut calls_weight_sum = Weight::zero();
for call in calls {
let call_weight = T::CapacityCalls::get_stable_weight(call)
.ok_or(ChargeFrqTransactionPaymentError::CallIsNotCapacityEligible.into())?;
calls_weight_sum = calls_weight_sum.saturating_add(call_weight);
}
let fee = Pallet::<T>::compute_capacity_fee(len as u32, calls_weight_sum);
T::OnChargeCapacityTransaction::can_withdraw_fee(who, fee.into())?;
Ok(fee)
}
fn dryrun_withdraw_token_fee(
&self,
who: &T::AccountId,
call: &<T as frame_system::Config>::RuntimeCall,
info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
len: usize,
tip: BalanceOf<T>,
) -> Result<BalanceOf<T>, TransactionValidityError> {
let fee = pallet_transaction_payment::Pallet::<T>::compute_fee(len as u32, info, tip);
if fee.is_zero() {
return Ok(Default::default());
}
<<T as pallet_transaction_payment::Config>::OnChargeTransaction as OnChargeTransaction<
T,
>>::can_withdraw_fee(who, call, info, fee, tip)?;
Ok(fee)
}
fn withdraw_fee(
&self,
who: &T::AccountId,
call: &<T as frame_system::Config>::RuntimeCall,
info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
len: usize,
) -> Result<(BalanceOf<T>, InitialPayment<T>), TransactionValidityError> {
match call.is_sub_type() {
Some(Call::pay_with_capacity { call }) =>
self.withdraw_capacity_fee(who, &vec![*call.clone()], len),
Some(Call::pay_with_capacity_batch_all { calls }) =>
self.withdraw_capacity_fee(who, calls, len),
_ => self.withdraw_token_fee(who, call, info, len, self.tip(call)),
}
}
fn withdraw_capacity_fee(
&self,
key: &T::AccountId,
calls: &Vec<<T as Config>::RuntimeCall>,
len: usize,
) -> Result<(BalanceOf<T>, InitialPayment<T>), TransactionValidityError> {
let mut calls_weight_sum = Weight::zero();
let mut subsidized_calls_weight_sum = Weight::zero();
for call in calls {
let call_weight = T::CapacityCalls::get_stable_weight(call)
.ok_or(ChargeFrqTransactionPaymentError::CallIsNotCapacityEligible.into())?;
calls_weight_sum = calls_weight_sum.saturating_add(call_weight);
if self.call_is_adding_eligible_key_to_msa(call) {
subsidized_calls_weight_sum =
subsidized_calls_weight_sum.saturating_add(call_weight);
}
}
let capacity_fee = Pallet::<T>::compute_capacity_fee(len as u32, calls_weight_sum)
.saturating_sub(Self::subsidized_calls_reduction(len, subsidized_calls_weight_sum));
let fee = T::OnChargeCapacityTransaction::withdraw_fee(key, capacity_fee.into())?;
Ok((fee.into(), InitialPayment::Capacity))
}
fn subsidized_calls_reduction(len: usize, eligible_call_weight: Weight) -> BalanceOf<T> {
if eligible_call_weight.is_zero() {
0u32.into()
} else {
let reduction: Permill = Permill::from_percent(70u32);
reduction.mul(Pallet::<T>::compute_capacity_fee(len as u32, eligible_call_weight))
}
}
fn call_is_adding_eligible_key_to_msa(&self, call: &<T as Config>::RuntimeCall) -> bool {
if let Some((owner_account_id, new_account_id, msa_id)) =
T::MsaCallFilter::get_add_key_data(call)
{
return T::MsaKeyProvider::key_eligible_for_subsidized_addition(
owner_account_id,
new_account_id,
msa_id,
);
}
false
}
fn withdraw_token_fee(
&self,
who: &T::AccountId,
call: &<T as frame_system::Config>::RuntimeCall,
info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
len: usize,
tip: BalanceOf<T>,
) -> Result<(BalanceOf<T>, InitialPayment<T>), TransactionValidityError> {
let fee = pallet_transaction_payment::Pallet::<T>::compute_fee(len as u32, info, tip);
if fee.is_zero() {
return Ok((fee, InitialPayment::Free));
}
<OnChargeTransactionOf<T> as OnChargeTransaction<T>>::withdraw_fee(
who, call, info, fee, tip,
)
.map(|i| (fee, InitialPayment::Token(i)))
.map_err(|_| -> TransactionValidityError { InvalidTransaction::Payment.into() })
}
}
impl<T: Config> core::fmt::Debug for ChargeFrqTransactionPayment<T> {
#[cfg(feature = "std")]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "ChargeFrqTransactionPayment<{:?}>", self.0)
}
#[cfg(not(feature = "std"))]
fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
Ok(())
}
}
#[derive(RuntimeDebugNoBound)]
pub enum Val<T: Config> {
Charge { tip: BalanceOf<T>, who: T::AccountId, fee: BalanceOf<T> },
NoCharge,
}
#[derive(RuntimeDebugNoBound)]
pub enum Pre<T: Config> {
Charge {
tip: BalanceOf<T>,
who: T::AccountId,
initial_payment: InitialPayment<T>,
weight: Weight,
},
NoCharge { refund: Weight },
}
impl<T: Config> TransactionExtension<<T as frame_system::Config>::RuntimeCall>
for ChargeFrqTransactionPayment<T>
where
<T as frame_system::Config>::RuntimeCall:
IsSubType<Call<T>> + Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
BalanceOf<T>: Send
+ Sync
+ FixedPointOperand
+ From<u64>
+ IsType<ChargeCapacityBalanceOf<T>>
+ IsType<CapacityBalanceOf<T>>,
<T as frame_system::Config>::RuntimeOrigin: AsSystemOriginSigner<T::AccountId> + Clone,
{
const IDENTIFIER: &'static str = "ChargeTransactionPayment";
type Implicit = ();
type Val = Val<T>;
type Pre = Pre<T>;
fn weight(&self, call: &<T as frame_system::Config>::RuntimeCall) -> Weight {
match call.is_sub_type() {
Some(Call::pay_with_capacity { .. }) |
Some(Call::pay_with_capacity_batch_all { .. }) =>
<T as Config>::WeightInfo::charge_tx_payment_capacity_based(),
_ => {
let info = call.get_dispatch_info();
if info.pays_fee == Pays::No {
<T as Config>::WeightInfo::charge_tx_payment_free()
} else {
<T as Config>::WeightInfo::charge_tx_payment_token_based()
}
},
}
}
fn validate(
&self,
origin: <T as frame_system::Config>::RuntimeOrigin,
call: &<T as frame_system::Config>::RuntimeCall,
info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
len: usize,
_self_implicit: Self::Implicit,
_inherited_implication: &impl Encode,
_source: TransactionSource,
) -> sp_runtime::traits::ValidateResult<Self::Val, <T as frame_system::Config>::RuntimeCall> {
let Some(who) = origin.as_system_origin_signer() else {
return Ok((
sp_runtime::transaction_validity::ValidTransaction::default(),
Val::NoCharge,
origin,
));
};
let fee = self.dryrun_withdraw_fee(who, call, info, len)?;
let tip = self.tip(call);
let priority = pallet_transaction_payment::ChargeTransactionPayment::<T>::get_priority(
info, len, tip, fee,
);
let val = Val::Charge { tip, who: who.clone(), fee };
let validity = ValidTransaction { priority, ..Default::default() };
Ok((validity, val, origin))
}
fn prepare(
self,
val: Self::Val,
_origin: &<T as frame_system::Config>::RuntimeOrigin,
call: &<T as frame_system::Config>::RuntimeCall,
info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
len: usize,
) -> Result<Self::Pre, TransactionValidityError> {
match val {
Val::Charge { tip, who, .. } => {
let (_fee, initial_payment) = self.withdraw_fee(&who, call, info, len)?;
Ok(Pre::Charge { tip, who, initial_payment, weight: self.weight(call) })
},
Val::NoCharge => Ok(Pre::NoCharge { refund: self.weight(call) }),
}
}
fn post_dispatch_details(
pre: Self::Pre,
info: &DispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
post_info: &PostDispatchInfoOf<<T as frame_system::Config>::RuntimeCall>,
len: usize,
result: &DispatchResult,
) -> Result<Weight, TransactionValidityError> {
match pre {
Pre::Charge { tip, who, initial_payment, .. } => match initial_payment {
InitialPayment::Token(already_withdrawn) => {
let weight = pallet_transaction_payment::ChargeTransactionPayment::<T>::post_dispatch_details(
pallet_transaction_payment::Pre::Charge { tip, who, imbalance: already_withdrawn },
info,
post_info,
len,
result,
)?;
Ok(weight)
},
InitialPayment::Capacity => {
debug_assert!(tip.is_zero(), "tip should be zero for Capacity tx.");
Ok(Weight::zero())
},
InitialPayment::Free => {
debug_assert!(tip.is_zero(), "tip should be zero if initial fee was zero.");
Ok(Weight::zero())
},
},
Pre::NoCharge { refund } => Ok(refund),
}
}
}