#![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::{DispatchInfoOf, Dispatchable, PostDispatchInfoOf, SignedExtension, Zero},
transaction_validity::{TransactionValidity, TransactionValidityError},
FixedPointOperand, Saturating,
};
use sp_std::prelude::*;
use common_primitives::{
capacity::{Nontransferable, Replenishable},
node::UtilityProvider,
};
pub use pallet::*;
pub use weights::*;
mod payment;
pub use payment::*;
pub use types::GetStableWeight;
pub mod types;
pub mod capacity_stable_weights;
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 {
match *self {
InitialPayment::Free => true,
_ => false,
}
}
pub fn is_capacity(&self) -> bool {
match *self {
InitialPayment::Capacity => true,
_ => false,
}
}
pub fn is_token(&self) -> bool {
match *self {
InitialPayment::Token(_) => true,
_ => false,
}
}
}
impl<T: Config> sp_std::fmt::Debug for InitialPayment<T> {
#[cfg(feature = "std")]
fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::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 sp_std::fmt::Formatter) -> sp_std::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::*;
#[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>;
}
#[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();
let capacity_overhead = Pallet::<T>::get_capacity_overhead_weight();
let total = capacity_overhead.saturating_add(dispatch_info.weight);
(< T as Config >::WeightInfo::pay_with_capacity().saturating_add(total), 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.weight)
.fold(Weight::zero(), |total: Weight, weight: Weight| total.saturating_add(weight));
let capacity_overhead = Pallet::<T>::get_capacity_overhead_weight();
let total = capacity_overhead.saturating_add(dispatch_weight);
(< T as Config >::WeightInfo::pay_with_capacity_batch_all(calls.len() as u32).saturating_add(total), 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);
let fee = base_fee.saturating_add(weight_fee).saturating_add(len_fee);
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, 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 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();
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);
let fee = T::OnChargeCapacityTransaction::withdraw_fee(key, fee.into())?;
Ok((fee.into(), InitialPayment::Capacity))
}
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> sp_std::fmt::Debug for ChargeFrqTransactionPayment<T> {
#[cfg(feature = "std")]
fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
write!(f, "ChargeFrqTransactionPayment<{:?}>", self.0)
}
#[cfg(not(feature = "std"))]
fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
Ok(())
}
}
impl<T: Config> SignedExtension 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>>,
{
const IDENTIFIER: &'static str = "ChargeTransactionPayment";
type AccountId = T::AccountId;
type Call = <T as frame_system::Config>::RuntimeCall;
type AdditionalSigned = ();
type Pre = (
BalanceOf<T>,
Self::AccountId,
InitialPayment<T>,
);
fn additional_signed(&self) -> Result<(), TransactionValidityError> {
Ok(())
}
fn validate(
&self,
who: &Self::AccountId,
call: &Self::Call,
info: &DispatchInfoOf<Self::Call>,
len: usize,
) -> TransactionValidity {
let (fee, _) = self.withdraw_fee(who, call, info, len)?;
let priority = pallet_transaction_payment::ChargeTransactionPayment::<T>::get_priority(
info,
len,
self.tip(call),
fee,
);
Ok(ValidTransaction { priority, ..Default::default() })
}
fn pre_dispatch(
self,
who: &Self::AccountId,
call: &Self::Call,
info: &DispatchInfoOf<Self::Call>,
len: usize,
) -> Result<Self::Pre, TransactionValidityError> {
let (_fee, initial_payment) = self.withdraw_fee(who, call, info, len)?;
Ok((self.tip(call), who.clone(), initial_payment))
}
fn post_dispatch(
maybe_pre: Option<Self::Pre>,
info: &DispatchInfoOf<Self::Call>,
post_info: &PostDispatchInfoOf<Self::Call>,
len: usize,
result: &DispatchResult,
) -> Result<(), TransactionValidityError> {
if let Some((tip, who, initial_payment)) = maybe_pre {
match initial_payment {
InitialPayment::Token(already_withdrawn) => {
pallet_transaction_payment::ChargeTransactionPayment::<T>::post_dispatch(
Some((tip, who, already_withdrawn)),
info,
post_info,
len,
result,
)?;
},
InitialPayment::Capacity => {
debug_assert!(tip.is_zero(), "tip should be zero for Capacity tx.");
},
InitialPayment::Free => {
debug_assert!(tip.is_zero(), "tip should be zero if initial fee was zero.");
},
}
}
Ok(())
}
}