1#![cfg_attr(not(feature = "std"), no_std)]
44
45extern crate alloc;
46
47use codec::{Decode, DecodeWithMemTracking, Encode};
48use frame_support::{
49 dispatch::{DispatchInfo, DispatchResult, PostDispatchInfo},
50 pallet_prelude::TransactionSource,
51 traits::IsType,
52 DefaultNoBound,
53};
54use pallet_transaction_payment::{ChargeTransactionPayment, OnChargeTransaction};
55use scale_info::TypeInfo;
56use sp_runtime::{
57 traits::{
58 AsSystemOriginSigner, DispatchInfoOf, Dispatchable, PostDispatchInfoOf, RefundWeight,
59 TransactionExtension, ValidateResult, Zero,
60 },
61 transaction_validity::{InvalidTransaction, TransactionValidityError, ValidTransaction},
62};
63
64#[cfg(test)]
65mod mock;
66#[cfg(test)]
67mod tests;
68pub mod weights;
69
70#[cfg(feature = "runtime-benchmarks")]
71mod benchmarking;
72
73mod payment;
74use frame_support::{pallet_prelude::Weight, traits::tokens::AssetId};
75pub use payment::*;
76pub use weights::WeightInfo;
77
78pub(crate) type BalanceOf<T> = <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::Balance;
80
81pub(crate) type OnChargeTransactionOf<T> =
83 <T as pallet_transaction_payment::Config>::OnChargeTransaction;
84
85pub(crate) type NativeLiquidityInfoOf<T> =
87 <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::LiquidityInfo;
88
89pub(crate) type AssetLiquidityInfoOf<T> =
91 <<T as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<T>>::LiquidityInfo;
92
93#[derive(Encode, Decode, DefaultNoBound, TypeInfo)]
95pub enum InitialPayment<T: Config> {
96 #[default]
98 Nothing,
99 Native(NativeLiquidityInfoOf<T>),
101 Asset((T::AssetId, AssetLiquidityInfoOf<T>)),
103}
104
105pub use pallet::*;
106
107#[frame_support::pallet]
108pub mod pallet {
109 use super::*;
110
111 #[pallet::config]
112 pub trait Config: frame_system::Config + pallet_transaction_payment::Config {
113 #[allow(deprecated)]
115 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
116 type AssetId: AssetId;
119 type OnChargeAssetTransaction: OnChargeAssetTransaction<
121 Self,
122 Balance = BalanceOf<Self>,
123 AssetId = Self::AssetId,
124 >;
125 type WeightInfo: WeightInfo;
127 #[cfg(feature = "runtime-benchmarks")]
128 type BenchmarkHelper: BenchmarkHelperTrait<
130 Self::AccountId,
131 Self::AssetId,
132 <<Self as Config>::OnChargeAssetTransaction as OnChargeAssetTransaction<Self>>::AssetId,
133 >;
134 }
135
136 #[pallet::pallet]
137 pub struct Pallet<T>(_);
138
139 #[cfg(feature = "runtime-benchmarks")]
140 pub trait BenchmarkHelperTrait<AccountId, FunAssetIdParameter, AssetIdParameter> {
142 fn create_asset_id_parameter(id: u32) -> (FunAssetIdParameter, AssetIdParameter);
144 fn setup_balances_and_pool(asset_id: FunAssetIdParameter, account: AccountId);
147 }
148
149 #[pallet::event]
150 #[pallet::generate_deposit(pub(super) fn deposit_event)]
151 pub enum Event<T: Config> {
152 AssetTxFeePaid {
155 who: T::AccountId,
156 actual_fee: BalanceOf<T>,
157 tip: BalanceOf<T>,
158 asset_id: T::AssetId,
159 },
160 AssetRefundFailed { native_amount_kept: BalanceOf<T> },
162 }
163}
164
165#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)]
177#[scale_info(skip_type_params(T))]
178pub struct ChargeAssetTxPayment<T: Config> {
179 #[codec(compact)]
180 tip: BalanceOf<T>,
181 asset_id: Option<T::AssetId>,
182}
183
184impl<T: Config> ChargeAssetTxPayment<T>
185where
186 T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
187{
188 pub fn from(tip: BalanceOf<T>, asset_id: Option<T::AssetId>) -> Self {
190 Self { tip, asset_id }
191 }
192
193 fn withdraw_fee(
196 &self,
197 who: &T::AccountId,
198 call: &T::RuntimeCall,
199 info: &DispatchInfoOf<T::RuntimeCall>,
200 fee: BalanceOf<T>,
201 ) -> Result<(BalanceOf<T>, InitialPayment<T>), TransactionValidityError> {
202 debug_assert!(self.tip <= fee, "tip should be included in the computed fee");
203 if fee.is_zero() {
204 Ok((fee, InitialPayment::Nothing))
205 } else if let Some(asset_id) = &self.asset_id {
206 T::OnChargeAssetTransaction::withdraw_fee(
207 who,
208 call,
209 info,
210 asset_id.clone(),
211 fee,
212 self.tip,
213 )
214 .map(|payment| (fee, InitialPayment::Asset((asset_id.clone(), payment))))
215 } else {
216 T::OnChargeTransaction::withdraw_fee(who, call, info, fee, self.tip)
217 .map(|payment| (fee, InitialPayment::Native(payment)))
218 }
219 }
220
221 fn can_withdraw_fee(
224 &self,
225 who: &T::AccountId,
226 call: &T::RuntimeCall,
227 info: &DispatchInfoOf<T::RuntimeCall>,
228 fee: BalanceOf<T>,
229 ) -> Result<(), TransactionValidityError> {
230 debug_assert!(self.tip <= fee, "tip should be included in the computed fee");
231 if fee.is_zero() {
232 Ok(())
233 } else if let Some(asset_id) = &self.asset_id {
234 T::OnChargeAssetTransaction::can_withdraw_fee(who, asset_id.clone(), fee.into())
235 } else {
236 <OnChargeTransactionOf<T> as OnChargeTransaction<T>>::can_withdraw_fee(
237 who, call, info, fee, self.tip,
238 )
239 .map_err(|_| -> TransactionValidityError { InvalidTransaction::Payment.into() })
240 }
241 }
242}
243
244impl<T: Config> core::fmt::Debug for ChargeAssetTxPayment<T> {
245 #[cfg(feature = "std")]
246 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
247 write!(f, "ChargeAssetTxPayment<{:?}, {:?}>", self.tip, self.asset_id.encode())
248 }
249 #[cfg(not(feature = "std"))]
250 fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
251 Ok(())
252 }
253}
254
255pub enum Val<T: Config> {
257 Charge {
258 tip: BalanceOf<T>,
259 who: T::AccountId,
261 fee: BalanceOf<T>,
263 },
264 NoCharge,
265}
266
267pub enum Pre<T: Config> {
270 Charge {
271 tip: BalanceOf<T>,
272 who: T::AccountId,
274 initial_payment: InitialPayment<T>,
276 weight: Weight,
278 },
279 NoCharge {
280 refund: Weight,
282 },
283}
284
285impl<T: Config> TransactionExtension<T::RuntimeCall> for ChargeAssetTxPayment<T>
286where
287 T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
288 BalanceOf<T>: Send + Sync + From<u64>,
289 T::AssetId: Send + Sync,
290 <T::RuntimeCall as Dispatchable>::RuntimeOrigin: AsSystemOriginSigner<T::AccountId> + Clone,
291{
292 const IDENTIFIER: &'static str = "ChargeAssetTxPayment";
293 type Implicit = ();
294 type Val = Val<T>;
295 type Pre = Pre<T>;
296
297 fn weight(&self, _: &T::RuntimeCall) -> Weight {
298 if self.asset_id.is_some() {
299 <T as Config>::WeightInfo::charge_asset_tx_payment_asset()
300 } else {
301 <T as Config>::WeightInfo::charge_asset_tx_payment_native()
302 }
303 }
304
305 fn validate(
306 &self,
307 origin: <T::RuntimeCall as Dispatchable>::RuntimeOrigin,
308 call: &T::RuntimeCall,
309 info: &DispatchInfoOf<T::RuntimeCall>,
310 len: usize,
311 _self_implicit: Self::Implicit,
312 _inherited_implication: &impl Encode,
313 _source: TransactionSource,
314 ) -> ValidateResult<Self::Val, T::RuntimeCall> {
315 let Some(who) = origin.as_system_origin_signer() else {
316 return Ok((ValidTransaction::default(), Val::NoCharge, origin))
317 };
318 let fee = pallet_transaction_payment::Pallet::<T>::compute_fee(len as u32, info, self.tip);
320 self.can_withdraw_fee(&who, call, info, fee)?;
321 let priority = ChargeTransactionPayment::<T>::get_priority(info, len, self.tip, fee);
322 let validity = ValidTransaction { priority, ..Default::default() };
323 let val = Val::Charge { tip: self.tip, who: who.clone(), fee };
324 Ok((validity, val, origin))
325 }
326
327 fn prepare(
328 self,
329 val: Self::Val,
330 _origin: &<T::RuntimeCall as Dispatchable>::RuntimeOrigin,
331 call: &T::RuntimeCall,
332 info: &DispatchInfoOf<T::RuntimeCall>,
333 _len: usize,
334 ) -> Result<Self::Pre, TransactionValidityError> {
335 match val {
336 Val::Charge { tip, who, fee } => {
337 let (_fee, initial_payment) = self.withdraw_fee(&who, call, info, fee)?;
339 Ok(Pre::Charge { tip, who, initial_payment, weight: self.weight(call) })
340 },
341 Val::NoCharge => Ok(Pre::NoCharge { refund: self.weight(call) }),
342 }
343 }
344
345 fn post_dispatch_details(
346 pre: Self::Pre,
347 info: &DispatchInfoOf<T::RuntimeCall>,
348 post_info: &PostDispatchInfoOf<T::RuntimeCall>,
349 len: usize,
350 _result: &DispatchResult,
351 ) -> Result<Weight, TransactionValidityError> {
352 let (tip, who, initial_payment, extension_weight) = match pre {
353 Pre::Charge { tip, who, initial_payment, weight } =>
354 (tip, who, initial_payment, weight),
355 Pre::NoCharge { refund } => {
356 return Ok(refund)
358 },
359 };
360
361 match initial_payment {
362 InitialPayment::Native(already_withdrawn) => {
363 let actual_ext_weight = <T as Config>::WeightInfo::charge_asset_tx_payment_native();
366 let unspent_weight = extension_weight.saturating_sub(actual_ext_weight);
367 let mut actual_post_info = *post_info;
368 actual_post_info.refund(unspent_weight);
369 let actual_fee = pallet_transaction_payment::Pallet::<T>::compute_actual_fee(
370 len as u32,
371 info,
372 &actual_post_info,
373 tip,
374 );
375 T::OnChargeTransaction::correct_and_deposit_fee(
376 &who,
377 info,
378 &actual_post_info,
379 actual_fee,
380 tip,
381 already_withdrawn,
382 )?;
383 pallet_transaction_payment::Pallet::<T>::deposit_fee_paid_event(
384 who, actual_fee, tip,
385 );
386 Ok(unspent_weight)
387 },
388 InitialPayment::Asset((asset_id, already_withdrawn)) => {
389 let actual_ext_weight = <T as Config>::WeightInfo::charge_asset_tx_payment_asset();
392 let unspent_weight = extension_weight.saturating_sub(actual_ext_weight);
393 let mut actual_post_info = *post_info;
394 actual_post_info.refund(unspent_weight);
395 let actual_fee = pallet_transaction_payment::Pallet::<T>::compute_actual_fee(
396 len as u32,
397 info,
398 &actual_post_info,
399 tip,
400 );
401 let converted_fee = T::OnChargeAssetTransaction::correct_and_deposit_fee(
402 &who,
403 info,
404 &actual_post_info,
405 actual_fee,
406 tip,
407 asset_id.clone(),
408 already_withdrawn,
409 )?;
410
411 Pallet::<T>::deposit_event(Event::<T>::AssetTxFeePaid {
412 who,
413 actual_fee: converted_fee,
414 tip,
415 asset_id,
416 });
417
418 Ok(unspent_weight)
419 },
420 InitialPayment::Nothing => {
421 debug_assert!(tip.is_zero(), "tip should be zero if initial fee was zero.");
426 Ok(extension_weight
427 .saturating_sub(<T as Config>::WeightInfo::charge_asset_tx_payment_zero()))
428 },
429 }
430 }
431}