code/runtime/src/transaction_payment_wrapper.rs

transaction_payment_wrapper.rs

957 lines · 36,315 bytes · 19a6485969RawGitHub
use crate::{NORMAL_DISPATCH_BASE_PRIORITY, OPERATIONAL_DISPATCH_PRIORITY, Weight};
use codec::{Decode, DecodeWithMemTracking, Encode};
use frame_election_provider_support::private::sp_arithmetic::traits::SaturatedConversion;
use frame_support::dispatch::{DispatchClass, DispatchInfo, PostDispatchInfo};
use frame_support::pallet_prelude::TypeInfo;
use frame_support::traits::{Get, IsSubType, IsType};
use pallet_subtensor_proxy as pallet_proxy;
use pallet_subtensor_utility as pallet_utility;
use pallet_transaction_payment::OnChargeTransaction;
use pallet_transaction_payment::{ChargeTransactionPayment, Config, Pre, Val};
use sp_runtime::DispatchResult;
use sp_runtime::traits::{
    AsSystemOriginSigner, DispatchInfoOf, DispatchOriginOf, Dispatchable, Implication,
    PostDispatchInfoOf, StaticLookup, TransactionExtension, TransactionExtensionMetadata,
    ValidateResult, Zero,
};
use sp_runtime::transaction_validity::{
    TransactionPriority, TransactionSource, TransactionValidity, TransactionValidityError,
};
use sp_std::boxed::Box;
use sp_std::vec::Vec;
use subtensor_macros::freeze_struct;

type BalanceOf<T> = <<T as Config>::OnChargeTransaction as OnChargeTransaction<T>>::Balance;

type RuntimeCallOf<T> = <T as frame_system::Config>::RuntimeCall;
type RuntimeOriginOf<T> = <T as frame_system::Config>::RuntimeOrigin;
type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
type LookupOf<T> = <T as frame_system::Config>::Lookup;

/// Runtime-supplied policy: which calls have their fee charged to the signing
/// hotkey's coldkey. Keeping the concrete list in the runtime (see `fee_filters`)
/// lets this generic extension stay free of a hardcoded allow-list.
pub trait ColdkeyFeeCallFilter<Call> {
    fn charges_coldkey(call: &Call) -> bool;
}

#[freeze_struct("f003cde1f9da4a90")]
#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct ChargeTransactionPaymentWrapper<T: Config> {
    inner: ChargeTransactionPayment<T>,
}

impl<T: Config> core::fmt::Debug for ChargeTransactionPaymentWrapper<T> {
    #[cfg(feature = "std")]
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
        write!(f, "ChargeTransactionPaymentWrapper",)
    }
    #[cfg(not(feature = "std"))]
    fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
        Ok(())
    }
}

impl<T: Config> ChargeTransactionPaymentWrapper<T>
where
    T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
    BalanceOf<T>: Send + Sync,
{
    pub fn new(fee: BalanceOf<T>) -> Self {
        let inner = ChargeTransactionPayment::<T>::from(fee);
        Self { inner }
    }
}

impl<T: Config + pallet_proxy::Config + pallet_utility::Config + pallet_subtensor::Config>
    ChargeTransactionPaymentWrapper<T>
where
    RuntimeCallOf<T>: IsSubType<pallet_proxy::Call<T>> + IsSubType<pallet_utility::Call<T>>,
    T: ColdkeyFeeCallFilter<RuntimeCallOf<T>>,
    RuntimeOriginOf<T>: AsSystemOriginSigner<AccountIdOf<T>> + Clone,
{
    /// Extract (real, delegate, inner_call) from a `proxy` call.
    /// `signer` is used as the delegate since it is implicit (the caller).
    /// `proxy_announced` is intentionally not handled here; fee propagation
    /// only applies to `proxy` calls to keep the logic simple.
    fn extract_proxy_parts<'a>(
        call: &'a RuntimeCallOf<T>,
        signer: &AccountIdOf<T>,
    ) -> Option<(
        AccountIdOf<T>,
        AccountIdOf<T>,
        &'a Box<<T as pallet_proxy::Config>::RuntimeCall>,
    )> {
        match call.is_sub_type()? {
            pallet_proxy::Call::proxy { real, call, .. } => {
                let real = LookupOf::<T>::lookup(real.clone()).ok()?;
                Some((real, signer.clone(), call))
            }
            _ => None,
        }
    }

    /// Determine who should pay the transaction fee for a proxy call.
    ///
    /// Follows the RealPaysFee chain up to 2 levels deep:
    /// - Case 1: `proxy(real=A, call)` → A pays if `RealPaysFee<A, signer>`
    /// - Case 2: `proxy(real=B, proxy(real=A, call))` → A pays if both
    ///   `RealPaysFee<B, signer>` and `RealPaysFee<A, B>` are set; B pays if only the former.
    /// - Case 3: `proxy(real=B, batch([proxy(real=A, ..), ..]))` → A pays if
    ///   `RealPaysFee<B, signer>`, all batch items are proxy calls with the same real A,
    ///   and `RealPaysFee<A, B>` is set; B pays if only the first condition holds.
    ///
    /// Returns `None` if the signer should pay (no RealPaysFee opt-in).
    fn extract_real_fee_payer(
        call: &RuntimeCallOf<T>,
        origin: &RuntimeOriginOf<T>,
    ) -> Option<AccountIdOf<T>> {
        let signer = origin.as_system_origin_signer()?;
        let (outer_real, delegate, inner_call) = Self::extract_proxy_parts(call, signer)?;

        // Check if the outer real account has opted in to pay for the delegate.
        if !pallet_proxy::Pallet::<T>::is_real_pays_fee(&outer_real, &delegate) {
            return None;
        }

        // outer_real pays. Try to push the fee deeper into nested proxy structures.
        let inner_call: &RuntimeCallOf<T> = (*inner_call).as_ref().into_ref();

        // Case 2: inner call is another proxy call.
        if let Some(inner_payer) = Self::extract_inner_proxy_payer(inner_call, &outer_real) {
            return Some(inner_payer);
        }

        // Case 3: inner call is a batch of proxy calls with the same real.
        if let Some(batch_payer) = Self::extract_batch_proxy_payer(inner_call, &outer_real) {
            return Some(batch_payer);
        }

        // Case 1: simple proxy, outer_real pays.
        Some(outer_real)
    }

    /// Check if an inner call is a proxy call where the inner real has opted in to pay.
    /// `outer_real` is used as the implicit delegate for `proxy` calls.
    fn extract_inner_proxy_payer(
        inner_call: &RuntimeCallOf<T>,
        outer_real: &AccountIdOf<T>,
    ) -> Option<AccountIdOf<T>> {
        let (inner_real, inner_delegate, _call) =
            Self::extract_proxy_parts(inner_call, outer_real)?;

        if pallet_proxy::Pallet::<T>::is_real_pays_fee(&inner_real, &inner_delegate) {
            Some(inner_real)
        } else {
            None
        }
    }

    /// Check if an inner call is a batch where ALL items are proxy calls with the same real
    /// account, and that real account has opted in to pay.
    /// `outer_real` is used as the implicit delegate for `proxy` calls within the batch.
    fn extract_batch_proxy_payer(
        inner_call: &RuntimeCallOf<T>,
        outer_real: &AccountIdOf<T>,
    ) -> Option<AccountIdOf<T>> {
        let calls: &Vec<<T as pallet_utility::Config>::RuntimeCall> =
            match inner_call.is_sub_type()? {
                pallet_utility::Call::batch { calls }
                | pallet_utility::Call::batch_all { calls }
                | pallet_utility::Call::force_batch { calls } => calls,
                _ => return None,
            };

        if calls.is_empty() {
            return None;
        }

        let mut common_real: Option<AccountIdOf<T>> = None;

        for call in calls.iter() {
            let call_ref: &RuntimeCallOf<T> = call.into_ref();
            let (inner_real, inner_delegate, _) = Self::extract_proxy_parts(call_ref, outer_real)?;

            match &common_real {
                None => {
                    // Check RealPaysFee once on the first item and memoize. For `proxy`
                    // calls the delegate is always `outer_real`, so a single read covers
                    // the entire batch; for `proxy_announced` it uses the explicit delegate.
                    if !pallet_proxy::Pallet::<T>::is_real_pays_fee(&inner_real, &inner_delegate) {
                        return None;
                    }
                    common_real = Some(inner_real);
                }
                // All items must share the same real account.
                Some(existing) if *existing != inner_real => return None,
                _ => {}
            }
        }

        common_real
    }

    /// Determine the coldkey that should pay the fee when a hotkey is the origin.
    ///
    /// Returns `Some(coldkey)` only when the call is runtime-marked as coldkey-paid
    /// (via [`ColdkeyFeeCallFilter`]) and the signer (hotkey) has an owner. The coldkey
    /// covers the protocol fee only; the signer-chosen tip is dropped by the caller
    /// (see `validate`) so a hotkey cannot spend coldkey funds through the tip. Returns
    /// `None` otherwise, so the signer pays.
    fn extract_coldkey_fee_payer(
        call: &RuntimeCallOf<T>,
        origin: &RuntimeOriginOf<T>,
    ) -> Option<AccountIdOf<T>> {
        if !T::charges_coldkey(call) {
            return None;
        }

        let signer = origin.as_system_origin_signer()?;
        pallet_subtensor::Pallet::<T>::maybe_coldkey_for_hotkey(signer)
    }
}

impl<T: Config + pallet_proxy::Config + pallet_utility::Config + pallet_subtensor::Config>
    TransactionExtension<RuntimeCallOf<T>> for ChargeTransactionPaymentWrapper<T>
where
    RuntimeCallOf<T>: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>
        + IsSubType<pallet_proxy::Call<T>>
        + IsSubType<pallet_utility::Call<T>>,
    T: ColdkeyFeeCallFilter<RuntimeCallOf<T>>,
    BalanceOf<T>: Zero + Send + Sync,
    RuntimeOriginOf<T>: AsSystemOriginSigner<AccountIdOf<T>>
        + Clone
        + From<frame_system::RawOrigin<AccountIdOf<T>>>,
{
    const IDENTIFIER: &'static str = "ChargeTransactionPaymentWrapper";
    type Implicit = ();
    type Val = Val<T>;
    type Pre = Pre<T>;

    fn weight(&self, call: &RuntimeCallOf<T>) -> Weight {
        // Account for up to 3 storage reads in the worst-case fee payer resolution
        // (outer is_real_pays_fee + inner/batch is_real_pays_fee + margin).
        self.inner
            .weight(call)
            .saturating_add(T::DbWeight::get().reads(3))
    }

    fn validate(
        &self,
        origin: DispatchOriginOf<RuntimeCallOf<T>>,
        call: &RuntimeCallOf<T>,
        info: &DispatchInfoOf<RuntimeCallOf<T>>,
        len: usize,
        self_implicit: Self::Implicit,
        inherited_implication: &impl Implication,
        source: TransactionSource,
    ) -> ValidateResult<Self::Val, RuntimeCallOf<T>> {
        let overridden_priority = {
            let base: TransactionPriority = match info.class {
                DispatchClass::Normal => NORMAL_DISPATCH_BASE_PRIORITY,
                DispatchClass::Mandatory => NORMAL_DISPATCH_BASE_PRIORITY,
                DispatchClass::Operational => OPERATIONAL_DISPATCH_PRIORITY,
            };
            base.saturated_into::<TransactionPriority>()
        };

        // Resolve the fee payer. A proxy `RealPaysFee` opt-in takes precedence; otherwise an
        // owned hotkey's coldkey pays for allow-listed calls. In that case the coldkey covers
        // the protocol fee only — the signer-chosen tip is dropped, since a tip does not buy
        // priority in this wrapper (priority is overridden above) and billing it to the coldkey
        // would let a hotkey drain coldkey funds. Other payers keep the original tip.
        let (fee_origin, tip) = if let Some(real) = Self::extract_real_fee_payer(call, &origin) {
            (
                frame_system::RawOrigin::Signed(real).into(),
                self.inner.tip(),
            )
        } else if let Some(coldkey) = Self::extract_coldkey_fee_payer(call, &origin) {
            (
                frame_system::RawOrigin::Signed(coldkey).into(),
                Zero::zero(),
            )
        } else {
            (origin.clone(), self.inner.tip())
        };

        let (mut valid_transaction, val, _fee_origin) = ChargeTransactionPayment::<T>::from(tip)
            .validate(
                fee_origin,
                call,
                info,
                len,
                self_implicit,
                inherited_implication,
                source,
            )?;

        valid_transaction.priority = overridden_priority;

        // Always return the original origin so the actual signer remains
        // the origin for dispatch and all subsequent extensions.
        Ok((valid_transaction, val, origin))
    }

    fn prepare(
        self,
        val: Self::Val,
        origin: &DispatchOriginOf<RuntimeCallOf<T>>,
        call: &RuntimeCallOf<T>,
        info: &DispatchInfoOf<RuntimeCallOf<T>>,
        len: usize,
    ) -> Result<Self::Pre, TransactionValidityError> {
        self.inner.prepare(val, origin, call, info, len)
    }

    fn metadata() -> Vec<TransactionExtensionMetadata> {
        ChargeTransactionPayment::<T>::metadata()
    }

    fn post_dispatch_details(
        pre: Self::Pre,
        info: &DispatchInfoOf<RuntimeCallOf<T>>,
        post_info: &PostDispatchInfoOf<RuntimeCallOf<T>>,
        len: usize,
        result: &DispatchResult,
    ) -> Result<Weight, TransactionValidityError> {
        ChargeTransactionPayment::<T>::post_dispatch_details(pre, info, post_info, len, result)
    }

    fn post_dispatch(
        pre: Self::Pre,
        info: &DispatchInfoOf<RuntimeCallOf<T>>,
        post_info: &mut PostDispatchInfoOf<RuntimeCallOf<T>>,
        len: usize,
        result: &DispatchResult,
    ) -> Result<(), TransactionValidityError> {
        ChargeTransactionPayment::<T>::post_dispatch(pre, info, post_info, len, result)
    }

    fn bare_validate(
        call: &RuntimeCallOf<T>,
        info: &DispatchInfoOf<RuntimeCallOf<T>>,
        len: usize,
    ) -> TransactionValidity {
        ChargeTransactionPayment::<T>::bare_validate(call, info, len)
    }

    fn bare_validate_and_prepare(
        call: &RuntimeCallOf<T>,
        info: &DispatchInfoOf<RuntimeCallOf<T>>,
        len: usize,
    ) -> Result<(), TransactionValidityError> {
        ChargeTransactionPayment::<T>::bare_validate_and_prepare(call, info, len)
    }

    fn bare_post_dispatch(
        info: &DispatchInfoOf<RuntimeCallOf<T>>,
        post_info: &mut PostDispatchInfoOf<RuntimeCallOf<T>>,
        len: usize,
        result: &DispatchResult,
    ) -> Result<(), TransactionValidityError> {
        ChargeTransactionPayment::<T>::bare_post_dispatch(info, post_info, len, result)
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use super::ChargeTransactionPaymentWrapper;
    use crate::{
        BuildStorage, NORMAL_DISPATCH_BASE_PRIORITY, OPERATIONAL_DISPATCH_PRIORITY, Proxy, Runtime,
        RuntimeCall, RuntimeGenesisConfig, RuntimeOrigin, System, SystemCall,
    };
    use frame_support::{
        assert_ok,
        dispatch::{DispatchClass, DispatchInfo, GetDispatchInfo, Pays},
    };
    use pallet_subtensor_proxy as pallet_proxy;
    use pallet_subtensor_utility as pallet_utility;
    use pallet_transaction_payment::Val;
    use sp_runtime::Saturating;
    use sp_runtime::traits::{DispatchTransaction, TransactionExtension, TxBaseImplication};
    use sp_runtime::transaction_validity::{
        TransactionSource, TransactionValidityError, ValidTransaction,
    };
    use subtensor_runtime_common::{AccountId, NetUid, ProxyType, TaoBalance};

    const SIGNER: [u8; 32] = [1_u8; 32];
    const REAL_A: [u8; 32] = [2_u8; 32];
    const REAL_B: [u8; 32] = [3_u8; 32];
    const OTHER: [u8; 32] = [4_u8; 32];
    const BALANCE: TaoBalance = TaoBalance::new(1_000_000_000_000_u64);

    fn new_test_ext() -> sp_io::TestExternalities {
        sp_tracing::try_init_simple();
        let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig {
            balances: pallet_balances::GenesisConfig {
                balances: vec![
                    (AccountId::from(SIGNER), BALANCE),
                    (AccountId::from(REAL_A), BALANCE),
                    (AccountId::from(REAL_B), BALANCE),
                    (AccountId::from(OTHER), BALANCE),
                ],
                dev_accounts: None,
            },
            ..Default::default()
        }
        .build_storage()
        .unwrap()
        .into();
        ext.execute_with(|| System::set_block_number(1));
        ext
    }

    fn signer() -> AccountId {
        AccountId::from(SIGNER)
    }
    fn real_a() -> AccountId {
        AccountId::from(REAL_A)
    }
    fn real_b() -> AccountId {
        AccountId::from(REAL_B)
    }
    fn other() -> AccountId {
        AccountId::from(OTHER)
    }

    // -- Call builders --

    fn call_remark() -> RuntimeCall {
        RuntimeCall::System(SystemCall::remark {
            remark: vec![1, 2, 3],
        })
    }

    fn call_set_weights() -> RuntimeCall {
        RuntimeCall::SubtensorModule(pallet_subtensor::Call::set_weights {
            netuid: NetUid::from(1),
            dests: vec![0],
            weights: vec![1],
            version_key: 0,
        })
    }

    fn call_commit_weights() -> RuntimeCall {
        RuntimeCall::SubtensorModule(pallet_subtensor::Call::commit_weights {
            netuid: NetUid::from(1),
            commit_hash: sp_core::H256::zero(),
        })
    }

    fn proxy_call(real: AccountId, inner: RuntimeCall) -> RuntimeCall {
        RuntimeCall::Proxy(pallet_proxy::Call::proxy {
            real: real.into(),
            force_proxy_type: None,
            call: Box::new(inner),
        })
    }

    fn proxy_announced_call(
        delegate: AccountId,
        real: AccountId,
        inner: RuntimeCall,
    ) -> RuntimeCall {
        RuntimeCall::Proxy(pallet_proxy::Call::proxy_announced {
            delegate: delegate.into(),
            real: real.into(),
            force_proxy_type: None,
            call: Box::new(inner),
        })
    }

    fn batch_call(calls: Vec<RuntimeCall>) -> RuntimeCall {
        RuntimeCall::Utility(pallet_utility::Call::batch { calls })
    }

    fn batch_all_call(calls: Vec<RuntimeCall>) -> RuntimeCall {
        RuntimeCall::Utility(pallet_utility::Call::batch_all { calls })
    }

    fn force_batch_call(calls: Vec<RuntimeCall>) -> RuntimeCall {
        RuntimeCall::Utility(pallet_utility::Call::force_batch { calls })
    }

    // -- Setup helpers --

    fn add_proxy(real: &AccountId, delegate: &AccountId) {
        assert_ok!(Proxy::add_proxy(
            RuntimeOrigin::signed(real.clone()),
            delegate.clone().into(),
            ProxyType::Any,
            0,
        ));
    }

    fn enable_real_pays_fee(real: &AccountId, delegate: &AccountId) {
        assert_ok!(Proxy::set_real_pays_fee(
            RuntimeOrigin::signed(real.clone()),
            delegate.clone().into(),
            true,
        ));
    }

    // -- Validate helpers --

    fn validate_call(
        origin: RuntimeOrigin,
        call: &RuntimeCall,
    ) -> Result<(ValidTransaction, Val<Runtime>), TransactionValidityError> {
        validate_call_with_info(origin, call, &call.get_dispatch_info())
    }

    fn validate_call_with_info(
        origin: RuntimeOrigin,
        call: &RuntimeCall,
        info: &DispatchInfo,
    ) -> Result<(ValidTransaction, Val<Runtime>), TransactionValidityError> {
        let ext = ChargeTransactionPaymentWrapper::<Runtime>::new(TaoBalance::new(0));
        let (valid_tx, val, _origin) = ext.validate(
            origin,
            call,
            info,
            100,
            (),
            &TxBaseImplication(()),
            TransactionSource::External,
        )?;
        Ok((valid_tx, val))
    }

    /// Extract the fee payer from the validate result.
    fn fee_payer(val: &Val<Runtime>) -> AccountId {
        match val {
            Val::Charge { who, .. } => who.clone(),
            _ => panic!("expected Val::Charge"),
        }
    }

    // ============================================================
    // Case 0: Non-proxy calls
    // ============================================================

    #[test]
    fn non_proxy_call_charges_signer() {
        new_test_ext().execute_with(|| {
            let call = call_remark();
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(fee_payer(&val), signer());
        });
    }

    // ============================================================
    // Case 1: Simple proxy (1 level)
    // ============================================================

    #[test]
    fn simple_proxy_charges_real_when_opted_in() {
        new_test_ext().execute_with(|| {
            add_proxy(&real_a(), &signer());
            enable_real_pays_fee(&real_a(), &signer());

            let call = proxy_call(real_a(), call_remark());
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(fee_payer(&val), real_a());
        });
    }

    #[test]
    fn simple_proxy_charges_signer_when_not_opted_in() {
        new_test_ext().execute_with(|| {
            add_proxy(&real_a(), &signer());
            // No enable_real_pays_fee

            let call = proxy_call(real_a(), call_remark());
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(fee_payer(&val), signer());
        });
    }

    #[test]
    fn proxy_announced_always_charges_signer() {
        new_test_ext().execute_with(|| {
            add_proxy(&real_a(), &real_b());
            enable_real_pays_fee(&real_a(), &real_b());

            // Fee propagation intentionally ignores proxy_announced; signer always pays.
            let call = proxy_announced_call(real_b(), real_a(), call_remark());
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(fee_payer(&val), signer());
        });
    }

    // ============================================================
    // Case 2: Nested proxy (2 levels)
    // ============================================================

    #[test]
    fn nested_proxy_charges_inner_real_when_both_opted_in() {
        new_test_ext().execute_with(|| {
            // Chain: signer → real_b → real_a
            add_proxy(&real_b(), &signer());
            enable_real_pays_fee(&real_b(), &signer());
            add_proxy(&real_a(), &real_b());
            enable_real_pays_fee(&real_a(), &real_b());

            let call = proxy_call(real_b(), proxy_call(real_a(), call_remark()));
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(fee_payer(&val), real_a());
        });
    }

    #[test]
    fn nested_proxy_charges_outer_real_when_only_outer_opted_in() {
        new_test_ext().execute_with(|| {
            add_proxy(&real_b(), &signer());
            enable_real_pays_fee(&real_b(), &signer());
            add_proxy(&real_a(), &real_b());
            // No enable_real_pays_fee for A→B

            let call = proxy_call(real_b(), proxy_call(real_a(), call_remark()));
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(fee_payer(&val), real_b());
        });
    }

    #[test]
    fn nested_proxy_charges_signer_when_neither_opted_in() {
        new_test_ext().execute_with(|| {
            add_proxy(&real_b(), &signer());
            add_proxy(&real_a(), &real_b());
            // No enable_real_pays_fee at all

            let call = proxy_call(real_b(), proxy_call(real_a(), call_remark()));
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(fee_payer(&val), signer());
        });
    }

    #[test]
    fn nested_proxy_charges_signer_when_only_inner_opted_in() {
        new_test_ext().execute_with(|| {
            add_proxy(&real_b(), &signer());
            // No enable_real_pays_fee for B→signer
            add_proxy(&real_a(), &real_b());
            enable_real_pays_fee(&real_a(), &real_b());

            let call = proxy_call(real_b(), proxy_call(real_a(), call_remark()));
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            // Outer RealPaysFee not set → signer pays (inner opt-in is irrelevant)
            assert_eq!(fee_payer(&val), signer());
        });
    }

    // ============================================================
    // Case 3: Batch of proxy calls
    // ============================================================

    #[test]
    fn batch_charges_inner_real_when_all_opted_in() {
        new_test_ext().execute_with(|| {
            add_proxy(&real_b(), &signer());
            enable_real_pays_fee(&real_b(), &signer());
            add_proxy(&real_a(), &real_b());
            enable_real_pays_fee(&real_a(), &real_b());

            let batch = batch_call(vec![
                proxy_call(real_a(), call_remark()),
                proxy_call(real_a(), call_remark()),
            ]);
            let call = proxy_call(real_b(), batch);
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(fee_payer(&val), real_a());
        });
    }

    #[test]
    fn batch_all_charges_inner_real_when_all_opted_in() {
        new_test_ext().execute_with(|| {
            add_proxy(&real_b(), &signer());
            enable_real_pays_fee(&real_b(), &signer());
            add_proxy(&real_a(), &real_b());
            enable_real_pays_fee(&real_a(), &real_b());

            let batch = batch_all_call(vec![
                proxy_call(real_a(), call_remark()),
                proxy_call(real_a(), call_remark()),
            ]);
            let call = proxy_call(real_b(), batch);
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(fee_payer(&val), real_a());
        });
    }

    #[test]
    fn force_batch_charges_inner_real_when_all_opted_in() {
        new_test_ext().execute_with(|| {
            add_proxy(&real_b(), &signer());
            enable_real_pays_fee(&real_b(), &signer());
            add_proxy(&real_a(), &real_b());
            enable_real_pays_fee(&real_a(), &real_b());

            let batch = force_batch_call(vec![
                proxy_call(real_a(), call_remark()),
                proxy_call(real_a(), call_remark()),
            ]);
            let call = proxy_call(real_b(), batch);
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(fee_payer(&val), real_a());
        });
    }

    #[test]
    fn batch_charges_outer_real_when_only_outer_opted_in() {
        new_test_ext().execute_with(|| {
            add_proxy(&real_b(), &signer());
            enable_real_pays_fee(&real_b(), &signer());
            add_proxy(&real_a(), &real_b());
            // No enable_real_pays_fee for A→B

            let batch = batch_call(vec![
                proxy_call(real_a(), call_remark()),
                proxy_call(real_a(), call_remark()),
            ]);
            let call = proxy_call(real_b(), batch);
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(fee_payer(&val), real_b());
        });
    }

    #[test]
    fn batch_charges_outer_real_when_mixed_inner_reals() {
        new_test_ext().execute_with(|| {
            add_proxy(&real_b(), &signer());
            enable_real_pays_fee(&real_b(), &signer());
            add_proxy(&real_a(), &real_b());
            enable_real_pays_fee(&real_a(), &real_b());
            add_proxy(&other(), &real_b());
            enable_real_pays_fee(&other(), &real_b());

            // Different inner reals → can't push deeper
            let batch = batch_call(vec![
                proxy_call(real_a(), call_remark()),
                proxy_call(other(), call_remark()),
            ]);
            let call = proxy_call(real_b(), batch);
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(fee_payer(&val), real_b());
        });
    }

    #[test]
    fn batch_charges_outer_real_when_non_proxy_in_batch() {
        new_test_ext().execute_with(|| {
            add_proxy(&real_b(), &signer());
            enable_real_pays_fee(&real_b(), &signer());

            // Batch contains a non-proxy call → extract_proxy_parts fails
            let batch = batch_call(vec![proxy_call(real_a(), call_remark()), call_remark()]);
            let call = proxy_call(real_b(), batch);
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(fee_payer(&val), real_b());
        });
    }

    #[test]
    fn batch_charges_outer_real_when_empty() {
        new_test_ext().execute_with(|| {
            add_proxy(&real_b(), &signer());
            enable_real_pays_fee(&real_b(), &signer());

            let batch = batch_call(vec![]);
            let call = proxy_call(real_b(), batch);
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(fee_payer(&val), real_b());
        });
    }

    #[test]
    fn batch_charges_outer_real_when_inner_real_not_opted_in() {
        new_test_ext().execute_with(|| {
            add_proxy(&real_b(), &signer());
            enable_real_pays_fee(&real_b(), &signer());
            add_proxy(&real_a(), &real_b());
            // real_a has NOT opted in to pay for real_b

            // Even with same real in all batch items, if RealPaysFee<A, B> not set → outer_real pays
            let batch = batch_call(vec![
                proxy_call(real_a(), call_remark()),
                proxy_call(real_a(), call_remark()),
            ]);
            let call = proxy_call(real_b(), batch);
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(fee_payer(&val), real_b());
        });
    }

    // ============================================================
    // Priority override
    // ============================================================

    #[test]
    fn priority_override_normal_dispatch() {
        new_test_ext().execute_with(|| {
            let call = call_remark();
            let (valid_tx, _val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            assert_eq!(valid_tx.priority, NORMAL_DISPATCH_BASE_PRIORITY);
        });
    }

    #[test]
    fn priority_override_operational_dispatch() {
        new_test_ext().execute_with(|| {
            let call = call_remark();
            let mut info = call.get_dispatch_info();
            info.class = DispatchClass::Operational;

            let (valid_tx, _val) =
                validate_call_with_info(RuntimeOrigin::signed(signer()), &call, &info).unwrap();
            assert_eq!(valid_tx.priority, OPERATIONAL_DISPATCH_PRIORITY);
        });
    }

    #[test]
    fn priority_override_mandatory_dispatch() {
        new_test_ext().execute_with(|| {
            let call = call_remark();
            let mut info = call.get_dispatch_info();
            info.class = DispatchClass::Mandatory;

            let (valid_tx, _val) =
                validate_call_with_info(RuntimeOrigin::signed(signer()), &call, &info).unwrap();
            // Mandatory uses the same base as Normal
            assert_eq!(valid_tx.priority, NORMAL_DISPATCH_BASE_PRIORITY);
        });
    }

    #[test]
    fn priority_override_applies_with_real_pays_fee() {
        new_test_ext().execute_with(|| {
            add_proxy(&real_a(), &signer());
            enable_real_pays_fee(&real_a(), &signer());

            let call = proxy_call(real_a(), call_remark());
            let (valid_tx, _val) = validate_call(RuntimeOrigin::signed(signer()), &call).unwrap();
            // Priority override should still apply when real pays fee
            assert_eq!(valid_tx.priority, NORMAL_DISPATCH_BASE_PRIORITY);
        });
    }

    // ============================================================
    // Coldkey pays the fee when a hotkey is the origin
    // ============================================================

    #[test]
    fn hotkey_with_owner_charges_coldkey() {
        new_test_ext().execute_with(|| {
            let hotkey = signer();
            let coldkey = other();

            pallet_subtensor::Owner::<Runtime>::insert(hotkey.clone(), coldkey.clone());

            let call = call_set_weights();
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(hotkey), &call).unwrap();

            assert_eq!(fee_payer(&val), coldkey);
        });
    }

    // Guards against the gate only recognizing `set_weights` rather than the whole group.
    #[test]
    fn other_group_member_charges_coldkey() {
        new_test_ext().execute_with(|| {
            let hotkey = signer();
            let coldkey = other();

            pallet_subtensor::Owner::<Runtime>::insert(hotkey.clone(), coldkey.clone());

            let call = call_commit_weights();
            let (_valid_tx, val) = validate_call(RuntimeOrigin::signed(hotkey), &call).unwrap();

            assert_eq!(fee_payer(&val), coldkey);
        });
    }

    #[test]
    fn hotkey_without_owner_charges_signer() {
        new_test_ext().execute_with(|| {
            let hotkey = signer();

            let call = call_set_weights();
            let (_valid_tx, val) =
                validate_call(RuntimeOrigin::signed(hotkey.clone()), &call).unwrap();

            assert_eq!(fee_payer(&val), hotkey);
        });
    }

    // Full lifecycle (validate → prepare → post_dispatch) via `DispatchTransaction::test_run`,
    // which validate-only tests cannot reach: a `Pays::Yes` allow-listed call debits the coldkey
    // and leaves the hotkey untouched, and a signer-chosen tip is excluded from the debit.
    #[test]
    fn full_lifecycle_debits_coldkey_excluding_tip() {
        new_test_ext().execute_with(|| {
            let hotkey = signer();
            let coldkey = other();

            pallet_subtensor::Owner::<Runtime>::insert(hotkey.clone(), coldkey.clone());

            let call = call_set_weights();
            // Force a fee-bearing call; the real `set_weights` is `Pays::No` in this PR.
            let info = DispatchInfo {
                pays_fee: Pays::Yes,
                ..call.get_dispatch_info()
            };

            let hotkey_before = pallet_balances::Pallet::<Runtime>::free_balance(&hotkey);
            let coldkey_before = pallet_balances::Pallet::<Runtime>::free_balance(&coldkey);

            // Sign with a large tip; it must not reach the coldkey.
            let ext = ChargeTransactionPaymentWrapper::<Runtime>::new(TaoBalance::new(1_000_000));
            assert_ok!(ext.test_run(
                RuntimeOrigin::signed(hotkey.clone()),
                &call,
                &info,
                0,
                0,
                |_origin| Ok(Default::default()),
            ));

            let tipless_fee = pallet_transaction_payment::Pallet::<Runtime>::compute_fee(
                0,
                &info,
                TaoBalance::new(0),
            );
            let coldkey_after = pallet_balances::Pallet::<Runtime>::free_balance(&coldkey);

            assert_eq!(
                pallet_balances::Pallet::<Runtime>::free_balance(&hotkey),
                hotkey_before
            );
            assert!(coldkey_after < coldkey_before, "coldkey should be debited");
            assert_eq!(
                coldkey_before.saturating_sub(coldkey_after),
                tipless_fee,
                "coldkey pays the tipless fee, not the tip"
            );
        });
    }

    // Owner is set, yet a call outside the group is never redirected: the signer pays.
    #[test]
    fn ineligible_call_charges_signer() {
        new_test_ext().execute_with(|| {
            let hotkey = signer();
            let coldkey = other();

            pallet_subtensor::Owner::<Runtime>::insert(hotkey.clone(), coldkey);

            let call = call_remark();
            let (_valid_tx, val) =
                validate_call(RuntimeOrigin::signed(hotkey.clone()), &call).unwrap();

            assert_eq!(fee_payer(&val), hotkey);
        });
    }
}