#![cfg_attr(not(feature = "std"), no_std)]
use sp_std::prelude::*;
use codec::{Decode, Encode};
use frame_support::{
dispatch::{DispatchInfo, DispatchResult, PostDispatchInfo},
traits::{
tokens::fungibles::{Balanced, Inspect},
IsType,
},
DefaultNoBound,
};
use pallet_transaction_payment::OnChargeTransaction;
use scale_info::TypeInfo;
use sp_runtime::{
traits::{DispatchInfoOf, Dispatchable, PostDispatchInfoOf, SignedExtension, Zero},
transaction_validity::{
InvalidTransaction, TransactionValidity, TransactionValidityError, ValidTransaction,
},
};
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
mod payment;
use frame_support::traits::tokens::AssetId;
use pallet_asset_conversion::MultiAssetIdConverter;
pub use payment::*;
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 AssetBalanceOf<T> =
<<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
pub(crate) type AssetIdOf<T> =
<<T as Config>::Fungibles as Inspect<<T as frame_system::Config>::AccountId>>::AssetId;
pub(crate) type ChargeAssetBalanceOf<T> =
<<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::Balance;
pub(crate) type ChargeAssetIdOf<T> =
<<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::AssetId;
pub(crate) type ChargeAssetLiquidityOf<T> =
<<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::LiquidityInfo;
#[derive(Encode, Decode, DefaultNoBound, TypeInfo)]
pub enum InitialPayment<T: Config> {
#[default]
Nothing,
Native(LiquidityInfoOf<T>),
Asset((LiquidityInfoOf<T>, BalanceOf<T>, AssetBalanceOf<T>)),
}
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
use super::*;
#[pallet::config]
pub trait Config:
frame_system::Config + pallet_transaction_payment::Config + pallet_asset_conversion::Config
{
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type Fungibles: Balanced<Self::AccountId>;
type OnChargeAssetTransaction: OnChargeAssetTransaction<Self>;
}
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
AssetTxFeePaid {
who: T::AccountId,
actual_fee: AssetBalanceOf<T>,
tip: BalanceOf<T>,
asset_id: ChargeAssetIdOf<T>,
},
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<ChargeAssetIdOf<T>>,
}
impl<T: Config> ChargeAssetTxPayment<T>
where
T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
AssetBalanceOf<T>: Send + Sync,
BalanceOf<T>: Send + Sync + Into<ChargeAssetBalanceOf<T>> + From<ChargeAssetLiquidityOf<T>>,
ChargeAssetIdOf<T>: Send + Sync,
{
pub fn from(tip: BalanceOf<T>, asset_id: Option<ChargeAssetIdOf<T>>) -> Self {
Self { tip, asset_id }
}
fn withdraw_fee(
&self,
who: &T::AccountId,
call: &T::RuntimeCall,
info: &DispatchInfoOf<T::RuntimeCall>,
len: usize,
) -> Result<(BalanceOf<T>, InitialPayment<T>), TransactionValidityError> {
let fee = pallet_transaction_payment::Pallet::<T>::compute_fee(len as u32, info, self.tip);
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.into(),
self.tip.into(),
)
.map(|(used_for_fee, received_exchanged, asset_consumed)| {
(
fee,
InitialPayment::Asset((
used_for_fee.into(),
received_exchanged.into(),
asset_consumed.into(),
)),
)
})
} else {
<OnChargeTransactionOf<T> as OnChargeTransaction<T>>::withdraw_fee(
who, call, info, fee, self.tip,
)
.map(|i| (fee, InitialPayment::Native(i)))
.map_err(|_| -> TransactionValidityError { InvalidTransaction::Payment.into() })
}
}
}
impl<T: Config> sp_std::fmt::Debug for ChargeAssetTxPayment<T> {
#[cfg(feature = "std")]
fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
write!(f, "ChargeAssetTxPayment<{:?}, {:?}>", self.tip, self.asset_id.encode())
}
#[cfg(not(feature = "std"))]
fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
Ok(())
}
}
impl<T: Config> SignedExtension for ChargeAssetTxPayment<T>
where
T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
AssetBalanceOf<T>: Send + Sync,
BalanceOf<T>: Send
+ Sync
+ From<u64>
+ Into<ChargeAssetBalanceOf<T>>
+ Into<ChargeAssetLiquidityOf<T>>
+ From<ChargeAssetLiquidityOf<T>>,
ChargeAssetIdOf<T>: Send + Sync,
{
const IDENTIFIER: &'static str = "ChargeAssetTxPayment";
type AccountId = T::AccountId;
type Call = T::RuntimeCall;
type AdditionalSigned = ();
type Pre = (
BalanceOf<T>,
Self::AccountId,
InitialPayment<T>,
Option<ChargeAssetIdOf<T>>,
);
fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> {
Ok(())
}
fn validate(
&self,
who: &Self::AccountId,
call: &Self::Call,
info: &DispatchInfoOf<Self::Call>,
len: usize,
) -> TransactionValidity {
use pallet_transaction_payment::ChargeTransactionPayment;
let (fee, _) = self.withdraw_fee(who, call, info, len)?;
let priority = ChargeTransactionPayment::<T>::get_priority(info, len, self.tip, 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, who.clone(), initial_payment, self.asset_id))
}
fn post_dispatch(
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, asset_id)) = pre {
match initial_payment {
InitialPayment::Native(already_withdrawn) => {
debug_assert!(
asset_id.is_none(),
"For that payment type the `asset_id` should be None"
);
pallet_transaction_payment::ChargeTransactionPayment::<T>::post_dispatch(
Some((tip, who, already_withdrawn)),
info,
post_info,
len,
result,
)?;
},
InitialPayment::Asset(already_withdrawn) => {
debug_assert!(
asset_id.is_some(),
"For that payment type the `asset_id` should be set"
);
let actual_fee = pallet_transaction_payment::Pallet::<T>::compute_actual_fee(
len as u32, info, post_info, tip,
);
if let Some(asset_id) = asset_id {
let (used_for_fee, received_exchanged, asset_consumed) = already_withdrawn;
let converted_fee = T::OnChargeAssetTransaction::correct_and_deposit_fee(
&who,
info,
post_info,
actual_fee.into(),
tip.into(),
used_for_fee.into(),
received_exchanged.into(),
asset_id.clone(),
asset_consumed.into(),
)?;
Pallet::<T>::deposit_event(Event::<T>::AssetTxFeePaid {
who,
actual_fee: converted_fee,
tip,
asset_id,
});
}
},
InitialPayment::Nothing => {
debug_assert!(tip.is_zero(), "tip should be zero if initial fee was zero.");
},
}
}
Ok(())
}
}