subspace_runtime/
fees.rs

1use crate::{Balances, Runtime, RuntimeCall, TransactionFees};
2use frame_support::traits::fungible::Inspect;
3#[cfg(feature = "runtime-benchmarks")]
4use frame_support::traits::fungible::Mutate;
5use frame_support::traits::tokens::WithdrawConsequence;
6use frame_support::traits::{Currency, ExistenceRequirement, Get, Imbalance, WithdrawReasons};
7use pallet_balances::NegativeImbalance;
8use parity_scale_codec::Encode;
9use sp_runtime::traits::{DispatchInfoOf, PostDispatchInfoOf, Zero};
10use sp_runtime::transaction_validity::{InvalidTransaction, TransactionValidityError};
11use subspace_runtime_primitives::{AccountId, Balance};
12
13pub struct TransactionByteFee;
14
15impl Get<Balance> for TransactionByteFee {
16    fn get() -> Balance {
17        TransactionFees::transaction_byte_fee()
18    }
19}
20
21pub struct LiquidityInfo {
22    storage_fee: Balance,
23    imbalance: NegativeImbalance<Runtime>,
24}
25
26/// Implementation of [`pallet_transaction_payment::OnChargeTransaction`] that charges transaction
27/// fees and distributes storage/compute fees and tip separately.
28pub struct OnChargeTransaction;
29
30// Required by OnChargeTransaction since stable2512. Credit = () because we don't
31// store withdrawn fees as a typed credit — they're burned/transferred directly.
32impl pallet_transaction_payment::TxCreditHold<Runtime> for OnChargeTransaction {
33    type Credit = ();
34}
35
36impl pallet_transaction_payment::OnChargeTransaction<Runtime> for OnChargeTransaction {
37    type LiquidityInfo = Option<LiquidityInfo>;
38    type Balance = Balance;
39
40    fn withdraw_fee(
41        who: &AccountId,
42        call: &RuntimeCall,
43        _info: &DispatchInfoOf<RuntimeCall>,
44        fee: Self::Balance,
45        tip: Self::Balance,
46    ) -> Result<Self::LiquidityInfo, TransactionValidityError> {
47        if fee.is_zero() {
48            return Ok(None);
49        }
50
51        let withdraw_reason = if tip.is_zero() {
52            WithdrawReasons::TRANSACTION_PAYMENT
53        } else {
54            WithdrawReasons::TRANSACTION_PAYMENT | WithdrawReasons::TIP
55        };
56
57        let withdraw_result = <Balances as Currency<AccountId>>::withdraw(
58            who,
59            fee,
60            withdraw_reason,
61            ExistenceRequirement::KeepAlive,
62        );
63        let imbalance = withdraw_result.map_err(|_error| InvalidTransaction::Payment)?;
64
65        // Separate storage fee while we have access to the call data structure to calculate it.
66        let storage_fee = TransactionByteFee::get()
67            * Balance::try_from(call.encoded_size())
68                .expect("Size of the call never exceeds balance units; qed");
69
70        Ok(Some(LiquidityInfo {
71            storage_fee,
72            imbalance,
73        }))
74    }
75
76    fn correct_and_deposit_fee(
77        who: &AccountId,
78        _dispatch_info: &DispatchInfoOf<RuntimeCall>,
79        _post_info: &PostDispatchInfoOf<RuntimeCall>,
80        corrected_fee: Self::Balance,
81        tip: Self::Balance,
82        liquidity_info: Self::LiquidityInfo,
83    ) -> Result<(), TransactionValidityError> {
84        if let Some(LiquidityInfo {
85            storage_fee,
86            imbalance,
87        }) = liquidity_info
88        {
89            // Calculate how much refund we should return
90            let refund_amount = imbalance.peek().saturating_sub(corrected_fee);
91            // Refund to the account that paid the fees. If this fails, the account might have
92            // dropped below the existential balance. In that case we don't refund anything.
93            let refund_imbalance = Balances::deposit_into_existing(who, refund_amount)
94                .unwrap_or_else(|_| <Balances as Currency<AccountId>>::PositiveImbalance::zero());
95            // Merge the imbalance caused by paying the fees and refunding parts of it again.
96            let adjusted_paid = imbalance
97                .offset(refund_imbalance)
98                .same()
99                .map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Payment))?;
100
101            // Split the tip from the total fee that ended up being paid.
102            let (tip, fee) = adjusted_paid.split(tip);
103            // Split paid storage and compute fees so that they can be distributed separately.
104            let (paid_storage_fee, paid_compute_fee) = fee.split(storage_fee);
105
106            TransactionFees::note_transaction_fees(
107                paid_storage_fee.peek(),
108                paid_compute_fee.peek(),
109                tip.peek(),
110            );
111        }
112        Ok(())
113    }
114
115    fn can_withdraw_fee(
116        who: &AccountId,
117        _call: &RuntimeCall,
118        _dispatch_info: &DispatchInfoOf<RuntimeCall>,
119        fee: Self::Balance,
120        _tip: Self::Balance,
121    ) -> Result<(), TransactionValidityError> {
122        if fee.is_zero() {
123            return Ok(());
124        }
125
126        match Balances::can_withdraw(who, fee) {
127            WithdrawConsequence::Success => Ok(()),
128            _ => Err(InvalidTransaction::Payment.into()),
129        }
130    }
131
132    #[cfg(feature = "runtime-benchmarks")]
133    fn endow_account(who: &AccountId, amount: Self::Balance) {
134        Balances::set_balance(who, amount);
135    }
136
137    #[cfg(feature = "runtime-benchmarks")]
138    fn minimum_balance() -> Self::Balance {
139        <Balances as Currency<AccountId>>::minimum_balance()
140    }
141}