#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use codec::{Decode, Encode};
use frame_support::{
dispatch::{DispatchInfo, DispatchResult, PostDispatchInfo},
pallet_prelude::TransactionSource,
traits::IsType,
DefaultNoBound,
};
use pallet_transaction_payment::{ChargeTransactionPayment, OnChargeTransaction};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{
AsSystemOriginSigner, DispatchInfoOf, Dispatchable, PostDispatchInfoOf, RefundWeight,
TransactionExtension, ValidateResult, Zero,
},
transaction_validity::{InvalidTransaction, TransactionValidityError, ValidTransaction},
};
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
pub mod weights;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
mod payment;
use frame_support::{pallet_prelude::Weight, traits::tokens::AssetId};
pub use payment::*;
pub use weights::WeightInfo;
pub(crate) type BalanceOf<T> = <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::Balance;
pub(crate) type OnChargeTransactionOf<T> =
<T as pallet_transaction_payment::Config>::OnChargeTransaction;
pub(crate) type NativeLiquidityInfoOf<T> =
<OnChargeTransactionOf<T> as OnChargeTransaction<T>>::LiquidityInfo;
pub(crate) type AssetLiquidityInfoOf<T> =
<<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::LiquidityInfo;
#[derive(Encode, Decode, DefaultNoBound, TypeInfo)]
pub enum InitialPayment<T: Config> {
#[default]
Nothing,
Native(NativeLiquidityInfoOf<T>),
Asset((T::AssetId, AssetLiquidityInfoOf<T>)),
}
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[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 AssetId: AssetId;
type OnChargeAssetTransaction: OnChargeAssetTransaction<
Self,
Balance = BalanceOf<Self>,
AssetId = Self::AssetId,
>;
type WeightInfo: WeightInfo;
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper: BenchmarkHelperTrait<
Self::AccountId,
Self::AssetId,
<<Self as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<Self>>::AssetId,
>;
}
#[pallet::pallet]
pub struct Pallet<T>(_);
#[cfg(feature = "runtime-benchmarks")]
pub trait BenchmarkHelperTrait<AccountId, FunAssetIdParameter, AssetIdParameter> {
fn create_asset_id_parameter(id: u32) -> (FunAssetIdParameter, AssetIdParameter);
fn setup_balances_and_pool(asset_id: FunAssetIdParameter, account: AccountId);
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
AssetTxFeePaid {
who: T::AccountId,
actual_fee: BalanceOf<T>,
tip: BalanceOf<T>,
asset_id: T::AssetId,
},
AssetRefundFailed { native_amount_kept: BalanceOf<T> },
}
}
#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct ChargeAssetTxPayment<T: Config> {
#[codec(compact)]
tip: BalanceOf<T>,
asset_id: Option<T::AssetId>,
}
impl<T: Config> ChargeAssetTxPayment<T>
where
T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
{
pub fn from(tip: BalanceOf<T>, asset_id: Option<T::AssetId>) -> Self {
Self { tip, asset_id }
}
fn withdraw_fee(
&self,
who: &T::AccountId,
call: &T::RuntimeCall,
info: &DispatchInfoOf<T::RuntimeCall>,
fee: BalanceOf<T>,
) -> Result<(BalanceOf<T>, InitialPayment<T>), TransactionValidityError> {
debug_assert!(self.tip <= fee, "tip should be included in the computed fee");
if fee.is_zero() {
Ok((fee, InitialPayment::Nothing))
} else if let Some(asset_id) = &self.asset_id {
T::OnChargeAssetTransaction::withdraw_fee(
who,
call,
info,
asset_id.clone(),
fee,
self.tip,
)
.map(|payment| (fee, InitialPayment::Asset((asset_id.clone(), payment))))
} else {
T::OnChargeTransaction::withdraw_fee(who, call, info, fee, self.tip)
.map(|payment| (fee, InitialPayment::Native(payment)))
}
}
fn can_withdraw_fee(
&self,
who: &T::AccountId,
call: &T::RuntimeCall,
info: &DispatchInfoOf<T::RuntimeCall>,
fee: BalanceOf<T>,
) -> Result<(), TransactionValidityError> {
debug_assert!(self.tip <= fee, "tip should be included in the computed fee");
if fee.is_zero() {
Ok(())
} else if let Some(asset_id) = &self.asset_id {
T::OnChargeAssetTransaction::can_withdraw_fee(who, asset_id.clone(), fee.into())
} else {
<OnChargeTransactionOf<T> as OnChargeTransaction<T>>::can_withdraw_fee(
who, call, info, fee, self.tip,
)
.map_err(|_| -> TransactionValidityError { InvalidTransaction::Payment.into() })
}
}
}
impl<T: Config> core::fmt::Debug for ChargeAssetTxPayment<T> {
#[cfg(feature = "std")]
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "ChargeAssetTxPayment<{:?}, {:?}>", self.tip, self.asset_id.encode())
}
#[cfg(not(feature = "std"))]
fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
Ok(())
}
}
pub enum Val<T: Config> {
Charge {
tip: BalanceOf<T>,
who: T::AccountId,
fee: BalanceOf<T>,
},
NoCharge,
}
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::RuntimeCall> for ChargeAssetTxPayment<T>
where
T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
BalanceOf<T>: Send + Sync + From<u64>,
T::AssetId: Send + Sync,
<T::RuntimeCall as Dispatchable>::RuntimeOrigin: AsSystemOriginSigner<T::AccountId> + Clone,
{
const IDENTIFIER: &'static str = "ChargeAssetTxPayment";
type Implicit = ();
type Val = Val<T>;
type Pre = Pre<T>;
fn weight(&self, _: &T::RuntimeCall) -> Weight {
if self.asset_id.is_some() {
<T as Config>::WeightInfo::charge_asset_tx_payment_asset()
} else {
<T as Config>::WeightInfo::charge_asset_tx_payment_native()
}
}
fn validate(
&self,
origin: <T::RuntimeCall as Dispatchable>::RuntimeOrigin,
call: &T::RuntimeCall,
info: &DispatchInfoOf<T::RuntimeCall>,
len: usize,
_self_implicit: Self::Implicit,
_inherited_implication: &impl Encode,
_source: TransactionSource,
) -> ValidateResult<Self::Val, T::RuntimeCall> {
let Some(who) = origin.as_system_origin_signer() else {
return Ok((ValidTransaction::default(), Val::NoCharge, origin))
};
let fee = pallet_transaction_payment::Pallet::<T>::compute_fee(len as u32, info, self.tip);
self.can_withdraw_fee(&who, call, info, fee)?;
let priority = ChargeTransactionPayment::<T>::get_priority(info, len, self.tip, fee);
let validity = ValidTransaction { priority, ..Default::default() };
let val = Val::Charge { tip: self.tip, who: who.clone(), fee };
Ok((validity, val, origin))
}
fn prepare(
self,
val: Self::Val,
_origin: &<T::RuntimeCall as Dispatchable>::RuntimeOrigin,
call: &T::RuntimeCall,
info: &DispatchInfoOf<T::RuntimeCall>,
_len: usize,
) -> Result<Self::Pre, TransactionValidityError> {
match val {
Val::Charge { tip, who, fee } => {
let (_fee, initial_payment) = self.withdraw_fee(&who, call, info, fee)?;
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::RuntimeCall>,
post_info: &PostDispatchInfoOf<T::RuntimeCall>,
len: usize,
_result: &DispatchResult,
) -> Result<Weight, TransactionValidityError> {
let (tip, who, initial_payment, extension_weight) = match pre {
Pre::Charge { tip, who, initial_payment, weight } =>
(tip, who, initial_payment, weight),
Pre::NoCharge { refund } => {
return Ok(refund)
},
};
match initial_payment {
InitialPayment::Native(already_withdrawn) => {
let actual_ext_weight = <T as Config>::WeightInfo::charge_asset_tx_payment_native();
let unspent_weight = extension_weight.saturating_sub(actual_ext_weight);
let mut actual_post_info = *post_info;
actual_post_info.refund(unspent_weight);
let actual_fee = pallet_transaction_payment::Pallet::<T>::compute_actual_fee(
len as u32,
info,
&actual_post_info,
tip,
);
T::OnChargeTransaction::correct_and_deposit_fee(
&who,
info,
&actual_post_info,
actual_fee,
tip,
already_withdrawn,
)?;
pallet_transaction_payment::Pallet::<T>::deposit_fee_paid_event(
who, actual_fee, tip,
);
Ok(unspent_weight)
},
InitialPayment::Asset((asset_id, already_withdrawn)) => {
let actual_ext_weight = <T as Config>::WeightInfo::charge_asset_tx_payment_asset();
let unspent_weight = extension_weight.saturating_sub(actual_ext_weight);
let mut actual_post_info = *post_info;
actual_post_info.refund(unspent_weight);
let actual_fee = pallet_transaction_payment::Pallet::<T>::compute_actual_fee(
len as u32,
info,
&actual_post_info,
tip,
);
let converted_fee = T::OnChargeAssetTransaction::correct_and_deposit_fee(
&who,
info,
&actual_post_info,
actual_fee,
tip,
asset_id.clone(),
already_withdrawn,
)?;
Pallet::<T>::deposit_event(Event::<T>::AssetTxFeePaid {
who,
actual_fee: converted_fee,
tip,
asset_id,
});
Ok(unspent_weight)
},
InitialPayment::Nothing => {
debug_assert!(tip.is_zero(), "tip should be zero if initial fee was zero.");
Ok(extension_weight
.saturating_sub(<T as Config>::WeightInfo::charge_asset_tx_payment_zero()))
},
}
}
}