pallet_xcm_transactor/
lib.rs

1// Copyright 2019-2025 PureStake Inc.
2// This file is part of Moonbeam.
3
4// Moonbeam is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Moonbeam is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16
17//! # Xcm Transactor Module
18//!
19//! ## Overview
20//!
21//! Module to provide transact capabilities on other chains
22//!
23//! In this pallet we will make distinctions between sovereign, derivative accounts and
24//! multilocation-based derived accounts. The first is the account the parachain controls
25//! in the destination chain, the second is an account derived from the
26//! sovereign account itself, e.g., by hashing it with an index, while the third is an account
27//! derived from the multilocation of a use in this chain (typically, hashing the ML).
28//! Such distinction is important since we want to keep the integrity of the sovereign account
29//!
30//! This pallet provides three ways of sending Transact operations to another chain
31//!
32//! - transact_through_derivative: Transact through an address derived from this chains sovereign
33//! 	account in the destination chain. For the transaction to successfully be dispatched in the
34//! 	destination chain, pallet-utility needs to be installed and at least paid xcm message
35//! 	execution should be allowed (and WithdrawAsset,BuyExecution and Transact messages allowed)
36//! 	in the destination chain
37//!
38//!
39//!
40//! 	The transactions are dispatched from a derivative account
41//! 	of the sovereign account
42//! 	This pallet only stores the index of the derivative account used, but
43//! 	not the derivative account itself. The only assumption this pallet makes
44//! 	is the existence of the pallet_utility pallet in the destination chain
45//! 	through the XcmTransact trait.
46//!
47//! 	All calls will be wrapped around utility::as_derivative. This makes sure
48//! 	the inner call is executed from the derivative account and not the sovereign
49//! 	account itself.
50//!
51//! 	Index registration happens through DerivativeAddressRegistrationOrigin.
52//! 	This derivative account can be funded by external users to
53//! 	ensure it has enough funds to make the calls
54//!
55//! - transact_through_sovereign: Transact through the sovereign account representing this chain.
56//! 	For the transaction to successfully be dispatched in the destination chain, at least paid
57//! 	xcm message execution should be allowed (and WithdrawAsset,BuyExecution and Transact
58//! 	messages allowed) in the destination chain. Only callable by Root
59//!
60//! - transact_through_signed: Transact through an account derived from the multilocation
61//! 	representing the signed user making the call. We ensure this by prepending DescendOrigin as
62//! 	the first instruction of the XCM message. For the transaction to successfully be dispatched
63//! 	in the destination chain, at least descended paid xcm message execution should be allowed
64//! 	(and DescendOrigin + WithdrawAsset + BuyExecution + Transact messages allowed) in the
65//! 	destination chain. Additionally, a ML-based derivation mechanism needs to be implemented
66//! 	in the destination chain.
67
68#![cfg_attr(not(feature = "std"), no_std)]
69
70use frame_support::pallet;
71use frame_support::weights::{constants::WEIGHT_REF_TIME_PER_SECOND, Weight};
72use sp_runtime::DispatchError;
73use xcm::latest::prelude::Location;
74use xcm_primitives::XcmFeeTrader;
75
76pub use pallet::*;
77
78#[cfg(any(test, feature = "runtime-benchmarks"))]
79mod benchmarks;
80
81#[cfg(test)]
82pub(crate) mod mock;
83#[cfg(test)]
84mod tests;
85
86pub mod encode;
87pub mod migrations;
88pub mod relay_indices;
89pub mod weights;
90pub use crate::weights::WeightInfo;
91
92type CurrencyIdOf<T> = <T as Config>::CurrencyId;
93
94/// TODO: Temporary (Will be removed when we have a proper way of getting pallet indices)
95/// Same index on both Polkadot and Kusama Asset Hub
96/// Kusama: https://github.com/polkadot-fellows/runtimes/blob/release-v2.0.0/system-parachains/asset-hubs/asset-hub-kusama/src/lib.rs#L1596
97/// Polkadot: https://github.com/polkadot-fellows/runtimes/blob/release-v2.0.0/system-parachains/asset-hubs/asset-hub-polkadot/src/lib.rs#L1400
98pub const ASSET_HUB_UTILITY_PALLET_INDEX: u8 = 40;
99
100/// TODO: Temporary (Will be removed when we have a proper way of getting pallet indices)
101/// Same index on both Polkadot and Kusama Asset Hub
102/// Kusama: https://github.com/polkadot-fellows/runtimes/blob/release-v2.0.0/system-parachains/asset-hubs/asset-hub-kusama/src/lib.rs#L1628
103/// Polkadot: https://github.com/polkadot-fellows/runtimes/blob/release-v2.0.0/system-parachains/asset-hubs/asset-hub-polkadot/src/lib.rs#L1434
104pub const ASSET_HUB_STAKING_PALLET_INDEX: u8 = 89;
105
106#[pallet]
107pub mod pallet {
108
109	use super::*;
110	use crate::relay_indices::RelayChainIndices;
111	use crate::weights::WeightInfo;
112	use crate::CurrencyIdOf;
113	use cumulus_primitives_core::{relay_chain::HrmpChannelId, ParaId};
114	use frame_support::traits::EitherOfDiverse;
115	use frame_support::{dispatch::DispatchResult, pallet_prelude::*};
116	use frame_system::{ensure_signed, pallet_prelude::*};
117	use sp_runtime::traits::{AtLeast32BitUnsigned, Convert};
118	use sp_std::boxed::Box;
119	use sp_std::convert::TryFrom;
120	use sp_std::prelude::*;
121	use sp_std::vec;
122	use sp_std::vec::Vec;
123	use xcm::{latest::prelude::*, VersionedLocation};
124	use xcm_executor::traits::{TransactAsset, WeightBounds};
125	use xcm_primitives::{
126		FilterMaxAssetFee, HrmpAvailableCalls, HrmpEncodeCall, UtilityAvailableCalls,
127		UtilityEncodeCall, XcmTransact,
128	};
129
130	#[pallet::pallet]
131	#[pallet::without_storage_info]
132	pub struct Pallet<T>(pub PhantomData<T>);
133
134	#[pallet::config]
135	pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
136		/// The balance type.
137		type Balance: Parameter
138			+ Member
139			+ AtLeast32BitUnsigned
140			+ Default
141			+ Copy
142			+ MaybeSerializeDeserialize
143			+ Into<u128>;
144
145		/// Currency Id.
146		type CurrencyId: Parameter + Member + Clone;
147
148		/// Convert `T::CurrencyId` to `Location`.
149		type CurrencyIdToLocation: Convert<Self::CurrencyId, Option<Location>>;
150
151		// XcmTransact needs to be implemented. This type needs to implement
152		// utility call encoding and multilocation gathering
153		type Transactor: Parameter + Member + Clone + XcmTransact;
154
155		/// AssetTransactor allows us to withdraw asset without being trapped
156		/// This should change in xcm v3, which allows us to burn assets
157		type AssetTransactor: TransactAsset;
158
159		// The origin that is allowed to register derivative address indices
160		type DerivativeAddressRegistrationOrigin: EnsureOrigin<Self::RuntimeOrigin>;
161
162		// The origin that is allowed to manipulate (open, close, accept, cancel) an Hrmp channel
163		type HrmpManipulatorOrigin: EnsureOrigin<Self::RuntimeOrigin>;
164
165		// The origin that is allowed to open and accept an Hrmp channel
166		type HrmpOpenOrigin: EnsureOrigin<Self::RuntimeOrigin>;
167
168		/// Convert `T::AccountId` to `Location`.
169		type AccountIdToLocation: Convert<Self::AccountId, Location>;
170
171		/// Means of measuring the weight consumed by an XCM message locally.
172		type Weigher: WeightBounds<Self::RuntimeCall>;
173
174		/// This chain's Universal Location.
175		type UniversalLocation: Get<InteriorLocation>;
176
177		/// Self chain location.
178		#[pallet::constant]
179		type SelfLocation: Get<Location>;
180
181		// The origin that is allowed to dispatch calls from the sovereign account directly
182		type SovereignAccountDispatcherOrigin: EnsureOrigin<Self::RuntimeOrigin>;
183
184		/// XCM sender.
185		type XcmSender: SendXcm;
186
187		// Base XCM weight.
188		///
189		/// The actual weight for an XCM message is `T::BaseXcmWeight +
190		/// T::Weigher::weight(&msg)`.
191		#[pallet::constant]
192		type BaseXcmWeight: Get<Weight>;
193
194		/// The way to retrieve the reserve of a Asset. This can be
195		/// configured to accept absolute or relative paths for self tokens
196		type ReserveProvider: xcm_primitives::Reserve;
197
198		/// The way to filter the max fee to use for HRMP management operations
199		type MaxHrmpFee: FilterMaxAssetFee;
200
201		/// Fee trader for computing XCM fees and managing asset pricing.
202		/// This replaces the old `DestinationAssetFeePerSecond` storage-based approach.
203		type FeeTrader: XcmFeeTrader;
204
205		type WeightInfo: WeightInfo;
206	}
207
208	/// Stores the information to be able to issue a transact operation in another chain use an
209	/// asset as fee payer.
210	#[derive(
211		Default,
212		Clone,
213		Encode,
214		Decode,
215		MaxEncodedLen,
216		RuntimeDebug,
217		Eq,
218		PartialEq,
219		scale_info::TypeInfo,
220		DecodeWithMemTracking,
221	)]
222	pub struct RemoteTransactInfoWithMaxWeight {
223		/// Extra weight that transacting a call in a destination chain adds
224		/// Extra weight involved when transacting without DescendOrigin
225		/// This should always be possible in a destination chain, since
226		/// it involves going through the sovereign account
227		pub transact_extra_weight: Weight,
228		/// Max destination weight
229		pub max_weight: Weight,
230		/// Whether we allow transacting through signed origins in another chain, and
231		/// how much extra cost implies
232		/// Extra weight involved when transacting with DescendOrigin
233		/// The reason for it being an option is because the destination chain
234		/// might not support constructing origins based on generic MultiLocations
235		pub transact_extra_weight_signed: Option<Weight>,
236	}
237
238	/// Enum defining the way to express a Currency.
239	#[derive(
240		Clone,
241		Encode,
242		Decode,
243		Eq,
244		PartialEq,
245		RuntimeDebug,
246		scale_info::TypeInfo,
247		DecodeWithMemTracking,
248	)]
249	pub enum Currency<CurrencyId> {
250		// Express the Currency as a CurrencyId
251		AsCurrencyId(CurrencyId),
252		// Express the Currency as its MultiLOcation
253		AsMultiLocation(Box<VersionedLocation>),
254	}
255
256	impl<T> Default for Currency<T> {
257		fn default() -> Currency<T> {
258			Currency::<T>::AsMultiLocation(Box::new(Location::default().into()))
259		}
260	}
261
262	#[derive(
263		Clone,
264		Encode,
265		Decode,
266		Eq,
267		PartialEq,
268		RuntimeDebug,
269		scale_info::TypeInfo,
270		DecodeWithMemTracking,
271	)]
272	pub struct HrmpInitParams {
273		pub para_id: ParaId,
274		pub proposed_max_capacity: u32,
275		pub proposed_max_message_size: u32,
276	}
277
278	/// Enum defining the way to express a Currency.
279	#[derive(
280		Clone,
281		Encode,
282		Decode,
283		Eq,
284		PartialEq,
285		RuntimeDebug,
286		scale_info::TypeInfo,
287		DecodeWithMemTracking,
288	)]
289	pub enum HrmpOperation {
290		InitOpen(HrmpInitParams),
291		Accept {
292			para_id: ParaId,
293		},
294		Close(HrmpChannelId),
295		Cancel {
296			channel_id: HrmpChannelId,
297			open_requests: u32,
298		},
299	}
300
301	#[derive(
302		Default,
303		Clone,
304		Encode,
305		Decode,
306		Eq,
307		PartialEq,
308		RuntimeDebug,
309		MaxEncodedLen,
310		scale_info::TypeInfo,
311		DecodeWithMemTracking,
312	)]
313
314	/// Struct that defines how to express the payment in a particular currency
315	/// currency is defined by the Currency enum, which can be expressed as:
316	/// - CurrencyId
317	/// - Location
318	///
319	/// The fee_amount is an option. In case of None, the fee will be tried to
320	/// be calculated from storage. If the storage item for the currency is not
321	/// populated, then it fails
322	pub struct CurrencyPayment<CurrencyId> {
323		// the currency in which we want to express our payment
324		pub currency: Currency<CurrencyId>,
325		// indicates whether we want to specify the fee amount to be used
326		pub fee_amount: Option<u128>,
327	}
328
329	#[derive(
330		Default,
331		Clone,
332		Encode,
333		Decode,
334		RuntimeDebug,
335		PartialEq,
336		scale_info::TypeInfo,
337		DecodeWithMemTracking,
338	)]
339	/// Struct tindicating information about transact weights
340	/// It allows to specify:
341	/// - transact_required_weight_at_most: the amount of weight the Transact instruction
342	///   should consume at most
343	/// - overall_weight: the overall weight to be used for the whole XCM message execution.
344	///   If None, then this amount will be tried to be derived from storage.  If the storage item
345	pub struct TransactWeights {
346		// the amount of weight the Transact instruction should consume at most
347		pub transact_required_weight_at_most: Weight,
348		// the overall weight to be used for the whole XCM message execution. If None,
349		// then this amount will be tried to be derived from storage.  If the storage item
350		// for the chain is not populated, then it fails
351		pub overall_weight: Option<WeightLimit>,
352	}
353
354	/// The amount of ref_time and proof_size to use for fee calculation if
355	/// we are dealing with an Unlimited variant inside 'overall_weight' field
356	/// of 'TransactWeights' struct.
357	pub const MAX_WEIGHT: Weight = Weight::from_parts(100_000_000_000, 100_000);
358
359	/// Since we are using pallet-utility for account derivation (through AsDerivative),
360	/// we need to provide an index for the account derivation. This storage item stores the index
361	/// assigned for a given local account. These indices are usable as derivative in the relay chain
362	#[pallet::storage]
363	#[pallet::getter(fn index_to_account)]
364	pub type IndexToAccount<T: Config> = StorageMap<_, Blake2_128Concat, u16, T::AccountId>;
365
366	/// Stores the transact info of a Location. This defines how much extra weight we need to
367	/// add when we want to transact in the destination chain and maximum amount of weight allowed
368	/// by the destination chain
369	#[pallet::storage]
370	#[pallet::getter(fn transact_info)]
371	pub type TransactInfoWithWeightLimit<T: Config> =
372		StorageMap<_, Blake2_128Concat, Location, RemoteTransactInfoWithMaxWeight>;
373
374	/// Stores the indices of relay chain pallets
375	#[pallet::storage]
376	#[pallet::getter(fn relay_indices)]
377	pub type RelayIndices<T: Config> = StorageValue<_, RelayChainIndices, ValueQuery>;
378
379	/// An error that can occur while executing the mapping pallet's logic.
380	#[pallet::error]
381	pub enum Error<T> {
382		IndexAlreadyClaimed,
383		UnclaimedIndex,
384		NotOwner,
385		UnweighableMessage,
386		CannotReanchor,
387		AssetHasNoReserve,
388		InvalidDest,
389		NotCrossChainTransfer,
390		AssetIsNotReserveInDestination,
391		DestinationNotInvertible,
392		ErrorDelivering,
393		DispatchWeightBiggerThanTotalWeight,
394		WeightOverflow,
395		AmountOverflow,
396		TransactorInfoNotSet,
397		NotCrossChainTransferableCurrency,
398		XcmExecuteError,
399		BadVersion,
400		MaxWeightTransactReached,
401		UnableToWithdrawAsset,
402		// Removed: FeePerSecondNotSet (was index 20)
403		// This error was removed. Fee-related errors are now handled by pallet-xcm-weight-trader.
404		#[codec(index = 21)]
405		SignedTransactNotAllowedForDestination,
406		#[codec(index = 22)]
407		FailedMultiLocationToJunction,
408		#[codec(index = 23)]
409		HrmpHandlerNotImplemented,
410		#[codec(index = 24)]
411		TooMuchFeeUsed,
412		#[codec(index = 25)]
413		ErrorValidating,
414		#[codec(index = 26)]
415		RefundNotSupportedWithTransactInfo,
416	}
417
418	#[pallet::event]
419	#[pallet::generate_deposit(pub(crate) fn deposit_event)]
420	pub enum Event<T: Config> {
421		/// Transacted the inner call through a derivative account in a destination chain.
422		TransactedDerivative {
423			account_id: T::AccountId,
424			dest: Location,
425			call: Vec<u8>,
426			index: u16,
427		},
428		/// Transacted the call through the sovereign account in a destination chain.
429		TransactedSovereign {
430			fee_payer: Option<T::AccountId>,
431			dest: Location,
432			call: Vec<u8>,
433		},
434		/// Transacted the call through a signed account in a destination chain.
435		TransactedSigned {
436			fee_payer: T::AccountId,
437			dest: Location,
438			call: Vec<u8>,
439		},
440		/// Registered a derivative index for an account id.
441		RegisteredDerivative {
442			account_id: T::AccountId,
443			index: u16,
444		},
445		DeRegisteredDerivative {
446			index: u16,
447		},
448		/// Transact failed
449		TransactFailed {
450			error: XcmError,
451		},
452		/// Changed the transact info of a location
453		TransactInfoChanged {
454			location: Location,
455			remote_info: RemoteTransactInfoWithMaxWeight,
456		},
457		/// Removed the transact info of a location
458		TransactInfoRemoved {
459			location: Location,
460		},
461		// Removed: DestFeePerSecondChanged (was index 8)
462		// This event was removed. Fee configuration events are now emitted by pallet-xcm-weight-trader.
463		// Removed: DestFeePerSecondRemoved (was index 9)
464		// This event was removed. Fee removal events are now emitted by pallet-xcm-weight-trader.
465		/// HRMP manage action succesfully sent
466		#[codec(index = 10)]
467		HrmpManagementSent {
468			action: HrmpOperation,
469		},
470	}
471
472	#[pallet::genesis_config]
473	pub struct GenesisConfig<T> {
474		pub relay_indices: RelayChainIndices,
475		#[serde(skip)]
476		pub _phantom: PhantomData<T>,
477	}
478
479	impl<T> Default for GenesisConfig<T> {
480		fn default() -> Self {
481			Self {
482				relay_indices: RelayChainIndices::default(),
483				_phantom: Default::default(),
484			}
485		}
486	}
487
488	#[pallet::genesis_build]
489	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
490		fn build(&self) {
491			<RelayIndices<T>>::put(self.relay_indices);
492		}
493	}
494
495	#[pallet::call]
496	impl<T: Config> Pallet<T> {
497		/// Register a derivative index for an account id. Dispatchable by
498		/// DerivativeAddressRegistrationOrigin
499		///
500		/// We do not store the derivative address, but only the index. We do not need to store
501		/// the derivative address to issue calls, only the index is enough
502		///
503		/// For now an index is registered for all possible destinations and not per-destination.
504		/// We can change this in the future although it would just make things more complicated
505		#[pallet::call_index(0)]
506		#[pallet::weight(T::WeightInfo::register())]
507		pub fn register(origin: OriginFor<T>, who: T::AccountId, index: u16) -> DispatchResult {
508			T::DerivativeAddressRegistrationOrigin::ensure_origin(origin)?;
509
510			ensure!(
511				IndexToAccount::<T>::get(&index).is_none(),
512				Error::<T>::IndexAlreadyClaimed
513			);
514
515			IndexToAccount::<T>::insert(&index, who.clone());
516
517			// Deposit event
518			Self::deposit_event(Event::<T>::RegisteredDerivative {
519				account_id: who,
520				index: index,
521			});
522
523			Ok(())
524		}
525
526		/// De-Register a derivative index. This prevents an account to use a derivative address
527		/// (represented by an index) from our of our sovereign accounts anymore
528		#[pallet::call_index(1)]
529		#[pallet::weight(T::WeightInfo::deregister())]
530		pub fn deregister(origin: OriginFor<T>, index: u16) -> DispatchResult {
531			T::DerivativeAddressRegistrationOrigin::ensure_origin(origin)?;
532
533			// Remove index
534			IndexToAccount::<T>::remove(&index);
535
536			// Deposit event
537			Self::deposit_event(Event::<T>::DeRegisteredDerivative { index });
538
539			Ok(())
540		}
541
542		/// Transact the inner call through a derivative account in a destination chain,
543		/// using 'fee_location' to pay for the fees. This fee_location is given as a multilocation
544		///
545		/// The caller needs to have the index registered in this pallet. The fee multiasset needs
546		/// to be a reserve asset for the destination transactor::multilocation.
547		#[pallet::call_index(2)]
548		#[pallet::weight(
549			Pallet::<T>::weight_of_initiate_reserve_withdraw()
550			.saturating_add(T::WeightInfo::transact_through_derivative())
551		)]
552		pub fn transact_through_derivative(
553			origin: OriginFor<T>,
554			// destination to which the message should be sent
555			dest: T::Transactor,
556			// derivative index to be used
557			index: u16,
558			// fee to be used
559			fee: CurrencyPayment<CurrencyIdOf<T>>,
560			// inner call to be executed in destination. This wiol
561			// be wrapped into utility.as_derivative
562			inner_call: Vec<u8>,
563			// weight information to be used
564			weight_info: TransactWeights,
565			// add RefundSurplus and DepositAsset appendix
566			refund: bool,
567		) -> DispatchResult {
568			let who = ensure_signed(origin)?;
569
570			let fee_location = Self::currency_to_multilocation(fee.currency)
571				.ok_or(Error::<T>::NotCrossChainTransferableCurrency)?;
572
573			// The index exists
574			let account = IndexToAccount::<T>::get(index).ok_or(Error::<T>::UnclaimedIndex)?;
575			// The derivative index is owned by the origin
576			ensure!(account == who, Error::<T>::NotOwner);
577
578			// Encode call bytes
579			// We make sure the inner call is wrapped on a as_derivative dispatchable
580			let call_bytes: Vec<u8> = <Self as UtilityEncodeCall>::encode_call(
581				dest.clone(),
582				UtilityAvailableCalls::AsDerivative(index, inner_call),
583			);
584
585			// Grab the destination
586			let dest = dest.destination();
587
588			// Calculate the total weight that the xcm message is going to spend in the
589			// destination chain
590			let total_weight = weight_info.overall_weight.map_or_else(
591				|| -> Result<_, DispatchError> {
592					let weight_info = Self::take_weight_from_transact_info(
593						dest.clone(),
594						weight_info.transact_required_weight_at_most,
595						refund,
596					)?;
597					Ok(WeightLimit::from(Some(weight_info)))
598				},
599				|v| Ok(v),
600			)?;
601
602			let total_weight_fee_calculation = match total_weight {
603				Unlimited => MAX_WEIGHT,
604				Limited(x) => x,
605			};
606
607			// Calculate fee based on FeePerSecond
608			let fee = Self::calculate_fee(
609				fee_location,
610				fee.fee_amount,
611				dest.clone(),
612				total_weight_fee_calculation,
613			)?;
614
615			// If refund is true, the appendix instruction will be a deposit back to the sovereign
616			let appendix = refund
617				.then(|| -> Result<_, DispatchError> {
618					Ok(vec![
619						RefundSurplus,
620						Self::deposit_instruction(T::SelfLocation::get(), &dest, 1u32)?,
621					])
622				})
623				.transpose()?;
624
625			Self::transact_in_dest_chain_asset_non_signed(
626				dest.clone(),
627				Some(who.clone()),
628				fee,
629				call_bytes.clone(),
630				OriginKind::SovereignAccount,
631				total_weight,
632				weight_info.transact_required_weight_at_most,
633				appendix,
634			)?;
635
636			// Deposit event
637			Self::deposit_event(Event::<T>::TransactedDerivative {
638				account_id: who,
639				dest,
640				call: call_bytes,
641				index,
642			});
643
644			Ok(())
645		}
646
647		/// Transact the call through the sovereign account in a destination chain,
648		/// 'fee_payer' pays for the fee
649		///
650		/// SovereignAccountDispatcherOrigin callable only
651		#[pallet::call_index(3)]
652		#[pallet::weight(
653			Pallet::<T>::weight_of_initiate_reserve_withdraw()
654			.saturating_add(T::WeightInfo::transact_through_sovereign())
655		)]
656		pub fn transact_through_sovereign(
657			origin: OriginFor<T>,
658			// destination to which the message should be sent
659			dest: Box<VersionedLocation>,
660			// account paying for fees
661			fee_payer: Option<T::AccountId>,
662			// fee to be used
663			fee: CurrencyPayment<CurrencyIdOf<T>>,
664			// call to be executed in destination
665			call: Vec<u8>,
666			// origin kind to be used
667			origin_kind: OriginKind,
668			// weight information to be used
669			weight_info: TransactWeights,
670			// add RefundSurplus and DepositAsset appendix
671			refund: bool,
672		) -> DispatchResult {
673			T::SovereignAccountDispatcherOrigin::ensure_origin(origin)?;
674
675			let fee_location = Self::currency_to_multilocation(fee.currency)
676				.ok_or(Error::<T>::NotCrossChainTransferableCurrency)?;
677
678			let dest = Location::try_from(*dest).map_err(|()| Error::<T>::BadVersion)?;
679
680			// Calculate the total weight that the xcm message is going to spend in the
681			// destination chain
682			let total_weight = weight_info.overall_weight.map_or_else(
683				|| -> Result<_, DispatchError> {
684					let weight_info = Self::take_weight_from_transact_info(
685						dest.clone(),
686						weight_info.transact_required_weight_at_most,
687						refund,
688					)?;
689					Ok(WeightLimit::from(Some(weight_info)))
690				},
691				|v| Ok(v),
692			)?;
693
694			let total_weight_fee_calculation = match total_weight {
695				Unlimited => MAX_WEIGHT,
696				Limited(x) => x,
697			};
698
699			// Calculate fee based on FeePerSecond and total_weight
700			let fee = Self::calculate_fee(
701				fee_location,
702				fee.fee_amount,
703				dest.clone(),
704				total_weight_fee_calculation,
705			)?;
706
707			// If refund is true, the appendix instruction will be a deposit back to the sovereign
708			let appendix = refund
709				.then(|| -> Result<_, DispatchError> {
710					Ok(vec![
711						RefundSurplus,
712						Self::deposit_instruction(T::SelfLocation::get(), &dest, 1u32)?,
713					])
714				})
715				.transpose()?;
716
717			// Grab the destination
718			Self::transact_in_dest_chain_asset_non_signed(
719				dest.clone(),
720				fee_payer.clone(),
721				fee,
722				call.clone(),
723				origin_kind,
724				total_weight,
725				weight_info.transact_required_weight_at_most,
726				appendix,
727			)?;
728
729			// Deposit event
730			Self::deposit_event(Event::<T>::TransactedSovereign {
731				fee_payer,
732				dest,
733				call,
734			});
735
736			Ok(())
737		}
738
739		/// Change the transact info of a location
740		#[pallet::call_index(4)]
741		#[pallet::weight(T::WeightInfo::set_transact_info())]
742		pub fn set_transact_info(
743			origin: OriginFor<T>,
744			location: Box<VersionedLocation>,
745			transact_extra_weight: Weight,
746			max_weight: Weight,
747			transact_extra_weight_signed: Option<Weight>,
748		) -> DispatchResult {
749			T::DerivativeAddressRegistrationOrigin::ensure_origin(origin)?;
750			let location = Location::try_from(*location).map_err(|()| Error::<T>::BadVersion)?;
751			let remote_info = RemoteTransactInfoWithMaxWeight {
752				transact_extra_weight,
753				max_weight,
754				transact_extra_weight_signed,
755			};
756
757			TransactInfoWithWeightLimit::<T>::insert(&location, &remote_info);
758
759			Self::deposit_event(Event::TransactInfoChanged {
760				location,
761				remote_info,
762			});
763			Ok(())
764		}
765
766		/// Remove the transact info of a location
767		#[pallet::call_index(5)]
768		#[pallet::weight(T::WeightInfo::remove_transact_info())]
769		pub fn remove_transact_info(
770			origin: OriginFor<T>,
771			location: Box<VersionedLocation>,
772		) -> DispatchResult {
773			T::DerivativeAddressRegistrationOrigin::ensure_origin(origin)?;
774			let location = Location::try_from(*location).map_err(|()| Error::<T>::BadVersion)?;
775
776			// Remove transact info
777			TransactInfoWithWeightLimit::<T>::remove(&location);
778
779			Self::deposit_event(Event::TransactInfoRemoved { location });
780			Ok(())
781		}
782
783		/// Transact the call through a signed origin in this chain
784		/// that should be converted to a transaction dispatch account in the destination chain
785		/// by any method implemented in the destination chains runtime
786		///
787		/// This time we are giving the currency as a currencyId instead of multilocation
788		#[pallet::call_index(6)]
789		#[pallet::weight(T::WeightInfo::transact_through_signed())]
790		pub fn transact_through_signed(
791			origin: OriginFor<T>,
792			// destination to which the message should be sent
793			dest: Box<VersionedLocation>,
794			// fee to be used
795			fee: CurrencyPayment<CurrencyIdOf<T>>,
796			// call to be executed in destination
797			call: Vec<u8>,
798			// weight information to be used
799			weight_info: TransactWeights,
800			// add RefundSurplus and DepositAsset appendix
801			refund: bool,
802		) -> DispatchResult {
803			let who = ensure_signed(origin)?;
804
805			let dest = Location::try_from(*dest).map_err(|()| Error::<T>::BadVersion)?;
806
807			let fee_location = Self::currency_to_multilocation(fee.currency)
808				.ok_or(Error::<T>::NotCrossChainTransferableCurrency)?;
809
810			// Calculate the total weight that the xcm message is going to spend in the
811			// destination chain
812			let total_weight = weight_info.overall_weight.map_or_else(
813				|| -> Result<_, DispatchError> {
814					let weight_info = Self::take_weight_from_transact_info_signed(
815						dest.clone(),
816						weight_info.transact_required_weight_at_most,
817						refund,
818					)?;
819					Ok(WeightLimit::from(Some(weight_info)))
820				},
821				|v| Ok(v),
822			)?;
823
824			let total_weight_fee_calculation = match total_weight {
825				Unlimited => MAX_WEIGHT,
826				Limited(x) => x,
827			};
828
829			// Fee to be paid
830			let fee = Self::calculate_fee(
831				fee_location,
832				fee.fee_amount,
833				dest.clone(),
834				total_weight_fee_calculation,
835			)?;
836
837			// If refund is true, the appendix instruction will be a deposit back to the sender
838			let appendix = refund
839				.then(|| -> Result<_, DispatchError> {
840					let sender = T::AccountIdToLocation::convert(who.clone());
841					Ok(vec![
842						RefundSurplus,
843						Self::deposit_instruction(sender, &dest, 1u32)?,
844					])
845				})
846				.transpose()?;
847
848			// Grab the destination
849			Self::transact_in_dest_chain_asset_signed(
850				dest.clone(),
851				who.clone(),
852				fee,
853				call.clone(),
854				OriginKind::SovereignAccount,
855				total_weight,
856				weight_info.transact_required_weight_at_most,
857				appendix,
858			)?;
859
860			// Deposit event
861			Self::deposit_event(Event::<T>::TransactedSigned {
862				fee_payer: who,
863				dest,
864				call,
865			});
866
867			Ok(())
868		}
869
870		// Removed: set_fee_per_second (was call_index 7)
871		// This extrinsic was removed. Fee configuration is now handled by pallet-xcm-weight-trader.
872
873		// Removed: remove_fee_per_second (was call_index 8)
874		// This extrinsic was removed. Fee removal is now handled by pallet-xcm-weight-trader.
875
876		/// Manage HRMP operations
877		#[pallet::call_index(9)]
878		#[pallet::weight(T::WeightInfo::hrmp_manage())]
879		pub fn hrmp_manage(
880			origin: OriginFor<T>,
881			action: HrmpOperation,
882			// fee to be used
883			fee: CurrencyPayment<CurrencyIdOf<T>>,
884			// weight information to be used
885			weight_info: TransactWeights,
886		) -> DispatchResult {
887			// WithdrawAsset
888			// BuyExecution
889			// SetAppendix(RefundSurplus, DepositAsset(sov account))
890			// Transact
891
892			// check permissions
893			match &action {
894				HrmpOperation::InitOpen(_) | HrmpOperation::Accept { .. } => {
895					<EitherOfDiverse<T::HrmpManipulatorOrigin, T::HrmpOpenOrigin>>::ensure_origin(
896						origin,
897					)?;
898				}
899				HrmpOperation::Close(_) | HrmpOperation::Cancel { .. } => {
900					T::HrmpManipulatorOrigin::ensure_origin(origin)?;
901				}
902			}
903
904			// process action
905			let call_bytes = match action.clone() {
906				HrmpOperation::InitOpen(params) => {
907					Self::hrmp_encode_call(HrmpAvailableCalls::InitOpenChannel(
908						params.para_id,
909						params.proposed_max_capacity,
910						params.proposed_max_message_size,
911					))
912				}
913				HrmpOperation::Accept { para_id } => {
914					Self::hrmp_encode_call(HrmpAvailableCalls::AcceptOpenChannel(para_id))
915				}
916				HrmpOperation::Close(close_params) => {
917					Self::hrmp_encode_call(HrmpAvailableCalls::CloseChannel(close_params))
918				}
919				HrmpOperation::Cancel {
920					channel_id,
921					open_requests,
922				} => Self::hrmp_encode_call(HrmpAvailableCalls::CancelOpenRequest(
923					channel_id,
924					open_requests,
925				)),
926			}
927			.map_err(|_| Error::<T>::HrmpHandlerNotImplemented)?;
928
929			let fee_location = Self::currency_to_multilocation(fee.currency)
930				.ok_or(Error::<T>::NotCrossChainTransferableCurrency)?;
931
932			// Grab the destination
933			// For hrmp, it is always parent
934			let destination = Location::parent();
935
936			// Calculate the total weight that the xcm message is going to spend in the
937			// destination chain
938			let total_weight = weight_info.overall_weight.map_or_else(
939				|| -> Result<_, DispatchError> {
940					let weight_info = Self::take_weight_from_transact_info(
941						destination.clone(),
942						weight_info.transact_required_weight_at_most,
943						false,
944					)?;
945					Ok(WeightLimit::from(Some(weight_info)))
946				},
947				|v| Ok(v),
948			)?;
949
950			let total_weight_fee_calculation = match total_weight {
951				Unlimited => MAX_WEIGHT,
952				Limited(x) => x,
953			};
954
955			let fee = Self::calculate_fee(
956				fee_location,
957				fee.fee_amount,
958				destination.clone(),
959				total_weight_fee_calculation,
960			)?;
961
962			ensure!(
963				T::MaxHrmpFee::filter_max_asset_fee(&fee),
964				Error::<T>::TooMuchFeeUsed
965			);
966
967			// The appendix instruction will be a deposit back to a self location
968			let deposit_appendix =
969				Self::deposit_instruction(T::SelfLocation::get(), &destination, 1u32)?;
970
971			Self::transact_in_dest_chain_asset_non_signed(
972				destination,
973				None,
974				fee,
975				call_bytes.clone(),
976				OriginKind::Native,
977				total_weight,
978				weight_info.transact_required_weight_at_most,
979				Some(vec![RefundSurplus, deposit_appendix]),
980			)?;
981
982			Self::deposit_event(Event::HrmpManagementSent { action });
983
984			Ok(())
985		}
986	}
987
988	impl<T: Config> Pallet<T> {
989		fn transact_in_dest_chain_asset_non_signed(
990			dest: Location,
991			fee_payer: Option<T::AccountId>,
992			fee: Asset,
993			call: Vec<u8>,
994			origin_kind: OriginKind,
995			total_weight: WeightLimit,
996			transact_required_weight_at_most: Weight,
997			with_appendix: Option<Vec<Instruction<()>>>,
998		) -> DispatchResult {
999			if let Some(fee_payer) = fee_payer {
1000				// Convert origin to multilocation
1001				let origin_as_mult = T::AccountIdToLocation::convert(fee_payer);
1002
1003				// Construct the local withdraw message with the previous calculated amount
1004				// This message deducts and burns "amount" from the caller when executed
1005				T::AssetTransactor::withdraw_asset(&fee.clone().into(), &origin_as_mult, None)
1006					.map_err(|_| Error::<T>::UnableToWithdrawAsset)?;
1007			}
1008
1009			// Construct the transact message. This is composed of WithdrawAsset||BuyExecution||
1010			// Transact.
1011			// WithdrawAsset: Withdraws "amount" from the sovereign account. These tokens will be
1012			// used to pay fees
1013			// BuyExecution: Buys "execution power" in the destination chain
1014			// Transact: Issues the transaction
1015			let transact_message: Xcm<()> = Self::transact_message(
1016				dest.clone(),
1017				fee,
1018				total_weight,
1019				call,
1020				transact_required_weight_at_most,
1021				origin_kind,
1022				with_appendix,
1023			)?;
1024
1025			// Send to sovereign
1026			let (ticket, _price) =
1027				T::XcmSender::validate(&mut Some(dest), &mut Some(transact_message))
1028					.map_err(|_| Error::<T>::ErrorValidating)?;
1029			T::XcmSender::deliver(ticket).map_err(|_| Error::<T>::ErrorDelivering)?;
1030
1031			Ok(())
1032		}
1033
1034		fn transact_in_dest_chain_asset_signed(
1035			dest: Location,
1036			fee_payer: T::AccountId,
1037			fee: Asset,
1038			call: Vec<u8>,
1039			origin_kind: OriginKind,
1040			total_weight: WeightLimit,
1041			transact_required_weight_at_most: Weight,
1042			with_appendix: Option<Vec<Instruction<()>>>,
1043		) -> DispatchResult {
1044			// Convert origin to multilocation
1045			let origin_as_mult = T::AccountIdToLocation::convert(fee_payer);
1046
1047			// Construct the transact message. This is composed of WithdrawAsset||BuyExecution||
1048			// Transact.
1049			// WithdrawAsset: Withdraws "amount" from the sovereign account. These tokens will be
1050			// used to pay fees
1051			// BuyExecution: Buys "execution power" in the destination chain
1052			// Transact: Issues the transaction
1053			let mut transact_message: Xcm<()> = Self::transact_message(
1054				dest.clone(),
1055				fee,
1056				total_weight,
1057				call,
1058				transact_required_weight_at_most,
1059				origin_kind,
1060				with_appendix,
1061			)?;
1062
1063			// We append DescendOrigin as the first instruction in the message
1064			// The new message looks like DescendOrigin||WithdrawAsset||BuyExecution||
1065			// Transact.
1066			let interior: Junctions = origin_as_mult
1067				.clone()
1068				.try_into()
1069				.map_err(|_| Error::<T>::FailedMultiLocationToJunction)?;
1070			transact_message.0.insert(0, DescendOrigin(interior));
1071
1072			// Send to destination chain
1073			let (ticket, _price) =
1074				T::XcmSender::validate(&mut Some(dest), &mut Some(transact_message))
1075					.map_err(|_| Error::<T>::ErrorValidating)?;
1076			T::XcmSender::deliver(ticket).map_err(|_| Error::<T>::ErrorDelivering)?;
1077
1078			Ok(())
1079		}
1080
1081		/// Calculate the amount of fee based on the multilocation of the fee asset and
1082		/// the total weight to be spent.
1083		/// This now delegates to the FeeTrader instead of reading from local storage.
1084		fn calculate_fee(
1085			fee_location: Location,
1086			fee_amount: Option<u128>,
1087			destination: Location,
1088			total_weight: Weight,
1089		) -> Result<Asset, DispatchError> {
1090			// If amount is explicitly provided, keep legacy behavior: do not enforce reserve/destination
1091			// validation here. Historically, the reserve check only ran when converting weight->fee.
1092			if let Some(amount) = fee_amount {
1093				return Ok(Asset {
1094					id: AssetId(fee_location),
1095					fun: Fungible(amount),
1096				});
1097			}
1098
1099			// Otherwise, delegate to FeeTrader to compute the fee based on weight and asset pricing,
1100			// and validate that the fee asset is a reserve for the destination.
1101			let amount: u128 = T::FeeTrader::compute_fee(total_weight, &fee_location)?;
1102
1103			let asset = Asset {
1104				id: AssetId(fee_location.clone()),
1105				fun: Fungible(amount),
1106			};
1107			let reserve = <T::ReserveProvider as xcm_primitives::Reserve>::reserve(&asset)
1108				.ok_or(Error::<T>::AssetHasNoReserve)?;
1109			if reserve != destination {
1110				return Err(Error::<T>::AssetIsNotReserveInDestination.into());
1111			}
1112
1113			Ok(asset)
1114		}
1115
1116		/// Construct the transact xcm message with the provided parameters
1117		fn transact_message(
1118			dest: Location,
1119			asset: Asset,
1120			dest_weight: WeightLimit,
1121			call: Vec<u8>,
1122			dispatch_weight: Weight,
1123			origin_kind: OriginKind,
1124			with_appendix: Option<Vec<Instruction<()>>>,
1125		) -> Result<Xcm<()>, DispatchError> {
1126			let mut instructions = vec![
1127				Self::withdraw_instruction(asset.clone(), &dest)?,
1128				Self::buy_execution(asset, &dest, dest_weight)?,
1129			];
1130			if let Some(appendix) = with_appendix {
1131				instructions.push(Self::appendix_instruction(appendix)?);
1132			}
1133			instructions.push(Transact {
1134				origin_kind,
1135				fallback_max_weight: Some(dispatch_weight),
1136				call: call.into(),
1137			});
1138			Ok(Xcm(instructions))
1139		}
1140
1141		/// Construct a buy execution xcm order with the provided parameters
1142		fn buy_execution(
1143			asset: Asset,
1144			at: &Location,
1145			weight: WeightLimit,
1146		) -> Result<Instruction<()>, DispatchError> {
1147			let universal_location = T::UniversalLocation::get();
1148			let fees = asset
1149				.reanchored(at, &universal_location)
1150				.map_err(|_| Error::<T>::CannotReanchor)?;
1151
1152			Ok(BuyExecution {
1153				fees,
1154				weight_limit: weight,
1155			})
1156		}
1157
1158		/// Construct a withdraw instruction from a sovereign account
1159		fn withdraw_instruction(
1160			asset: Asset,
1161			at: &Location,
1162		) -> Result<Instruction<()>, DispatchError> {
1163			let universal_location = T::UniversalLocation::get();
1164			let fees = asset
1165				.reanchored(at, &universal_location)
1166				.map_err(|_| Error::<T>::CannotReanchor)?;
1167
1168			Ok(WithdrawAsset(fees.into()))
1169		}
1170
1171		/// Construct a deposit instruction to a sovereign account
1172		fn deposit_instruction(
1173			mut beneficiary: Location,
1174			at: &Location,
1175			max_assets: u32,
1176		) -> Result<Instruction<()>, DispatchError> {
1177			let universal_location = T::UniversalLocation::get();
1178			beneficiary
1179				.reanchor(at, &universal_location)
1180				.map_err(|_| Error::<T>::CannotReanchor)?;
1181			Ok(DepositAsset {
1182				assets: Wild(AllCounted(max_assets)),
1183				beneficiary,
1184			})
1185		}
1186
1187		/// Construct a withdraw instruction from a sovereign account
1188		fn appendix_instruction(
1189			instructions: Vec<Instruction<()>>,
1190		) -> Result<Instruction<()>, DispatchError> {
1191			Ok(SetAppendix(Xcm(instructions)))
1192		}
1193
1194		/// Returns weight of `weight_of_initiate_reserve_withdraw` call.
1195		fn weight_of_initiate_reserve_withdraw() -> Weight {
1196			let dest = Location::parent();
1197
1198			// We can use whatever asset here
1199			let asset = Location::parent();
1200
1201			// Construct Asset
1202			let fee = Asset {
1203				id: AssetId(asset.clone()),
1204				fun: Fungible(0),
1205			};
1206
1207			let xcm: Xcm<()> = Xcm(vec![
1208				WithdrawAsset(fee.into()),
1209				InitiateReserveWithdraw {
1210					assets: AssetFilter::Wild(All),
1211					reserve: dest.clone(),
1212					xcm: Xcm(vec![]),
1213				},
1214			]);
1215			T::Weigher::weight(&mut xcm.into(), Weight::MAX)
1216				.map_or(Weight::MAX, |w| T::BaseXcmWeight::get().saturating_add(w))
1217		}
1218
1219		/// Returns the weight information for a destination from storage
1220		/// it returns the weight to be used in non-signed cases
1221		pub fn take_weight_from_transact_info(
1222			dest: Location,
1223			dest_weight: Weight,
1224			refund: bool,
1225		) -> Result<Weight, DispatchError> {
1226			// this is due to TransactInfo only has info of cases where RefundSurplus is not used
1227			// so we have to ensure 'refund' is false
1228			ensure!(!refund, Error::<T>::RefundNotSupportedWithTransactInfo);
1229			// Grab transact info for the destination provided
1230			let transactor_info = TransactInfoWithWeightLimit::<T>::get(&dest)
1231				.ok_or(Error::<T>::TransactorInfoNotSet)?;
1232
1233			let total_weight = dest_weight
1234				.checked_add(&transactor_info.transact_extra_weight)
1235				.ok_or(Error::<T>::WeightOverflow)?;
1236
1237			ensure!(
1238				total_weight.all_lte(transactor_info.max_weight),
1239				Error::<T>::MaxWeightTransactReached
1240			);
1241			Ok(total_weight)
1242		}
1243
1244		/// Returns the weight information for a destination from storage
1245		/// it returns the weight to be used in signed cases
1246		pub fn take_weight_from_transact_info_signed(
1247			dest: Location,
1248			dest_weight: Weight,
1249			refund: bool,
1250		) -> Result<Weight, DispatchError> {
1251			// this is due to TransactInfo only has info of cases where RefundSurplus is not used
1252			// so we have to ensure 'refund' is false
1253			ensure!(!refund, Error::<T>::RefundNotSupportedWithTransactInfo);
1254			// Grab transact info for the destination provided
1255			let transactor_info = TransactInfoWithWeightLimit::<T>::get(&dest)
1256				.ok_or(Error::<T>::TransactorInfoNotSet)?;
1257
1258			// If this storage item is not set, it means that the destination chain
1259			// does not support this kind of transact message
1260			let transact_in_dest_as_signed_weight = transactor_info
1261				.transact_extra_weight_signed
1262				.ok_or(Error::<T>::SignedTransactNotAllowedForDestination)?;
1263
1264			let total_weight = dest_weight
1265				.checked_add(&transact_in_dest_as_signed_weight)
1266				.ok_or(Error::<T>::WeightOverflow)?;
1267
1268			ensure!(
1269				total_weight.all_lte(transactor_info.max_weight),
1270				Error::<T>::MaxWeightTransactReached
1271			);
1272			Ok(total_weight)
1273		}
1274
1275		/// Get the fee per second for an asset location.
1276		///
1277		/// This derives the fee-per-second value by asking the configured `FeeTrader`
1278		/// to compute the fee for one second of execution weight, instead of
1279		/// exposing any internal pricing representation (such as relative prices).
1280		pub fn dest_asset_fee_per_second(location: &Location) -> Option<u128> {
1281			let one_second = Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND, 0);
1282			T::FeeTrader::compute_fee(one_second, location).ok()
1283		}
1284
1285		/// Converts Currency to multilocation
1286		pub fn currency_to_multilocation(currency: Currency<CurrencyIdOf<T>>) -> Option<Location> {
1287			match currency {
1288				Currency::AsCurrencyId(id) => T::CurrencyIdToLocation::convert(id),
1289				Currency::AsMultiLocation(multiloc) => Location::try_from(*multiloc).ok(),
1290			}
1291		}
1292	}
1293}