xcm_primitives/
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//! The XCM primitive trait implementations
18
19#![cfg_attr(not(feature = "std"), no_std)]
20
21mod asset_id_conversions;
22pub use asset_id_conversions::*;
23
24mod constants;
25pub use constants::*;
26
27mod ethereum_xcm;
28pub use ethereum_xcm::*;
29
30mod filter_asset_max_fee;
31pub use filter_asset_max_fee::*;
32
33mod origin_conversion;
34pub use origin_conversion::*;
35
36mod transactor_traits;
37pub use transactor_traits::*;
38
39mod fee_trader;
40pub use fee_trader::*;
41
42use sp_std::sync::Arc;
43use sp_std::vec::Vec;
44use xcm::latest::{Junction, Junctions, Location};
45
46/// Build Junctions from a slice of junctions
47fn junctions_from_slice(junctions: &[Junction]) -> Option<Junctions> {
48	match junctions.len() {
49		0 => Some(Junctions::Here),
50		1 => Some(Junctions::X1(Arc::new([junctions[0].clone()]))),
51		2 => Some(Junctions::X2(Arc::new([
52			junctions[0].clone(),
53			junctions[1].clone(),
54		]))),
55		3 => Some(Junctions::X3(Arc::new([
56			junctions[0].clone(),
57			junctions[1].clone(),
58			junctions[2].clone(),
59		]))),
60		4 => Some(Junctions::X4(Arc::new([
61			junctions[0].clone(),
62			junctions[1].clone(),
63			junctions[2].clone(),
64			junctions[3].clone(),
65		]))),
66		5 => Some(Junctions::X5(Arc::new([
67			junctions[0].clone(),
68			junctions[1].clone(),
69			junctions[2].clone(),
70			junctions[3].clone(),
71			junctions[4].clone(),
72		]))),
73		6 => Some(Junctions::X6(Arc::new([
74			junctions[0].clone(),
75			junctions[1].clone(),
76			junctions[2].clone(),
77			junctions[3].clone(),
78			junctions[4].clone(),
79			junctions[5].clone(),
80		]))),
81		7 => Some(Junctions::X7(Arc::new([
82			junctions[0].clone(),
83			junctions[1].clone(),
84			junctions[2].clone(),
85			junctions[3].clone(),
86			junctions[4].clone(),
87			junctions[5].clone(),
88			junctions[6].clone(),
89		]))),
90		8 => Some(Junctions::X8(Arc::new([
91			junctions[0].clone(),
92			junctions[1].clone(),
93			junctions[2].clone(),
94			junctions[3].clone(),
95			junctions[4].clone(),
96			junctions[5].clone(),
97			junctions[6].clone(),
98			junctions[7].clone(),
99		]))),
100		_ => None,
101	}
102}
103
104pub fn split_location_into_chain_part_and_beneficiary(
105	mut location: Location,
106) -> Option<(Location, Location)> {
107	let mut beneficiary_junctions_vec = Vec::new();
108
109	// start popping junctions until we reach chain identifier
110	while let Some(j) = location.last() {
111		if matches!(j, Junction::Parachain(_) | Junction::GlobalConsensus(_)) {
112			// return chain subsection
113			// Reverse the vec to restore original order, then build Junctions
114			beneficiary_junctions_vec.reverse();
115			let beneficiary_junctions = junctions_from_slice(&beneficiary_junctions_vec)?;
116			return Some((location, beneficiary_junctions.into_location()));
117		} else {
118			let (location_prefix, maybe_last_junction) = location.split_last_interior();
119			location = location_prefix;
120			if let Some(junction) = maybe_last_junction {
121				beneficiary_junctions_vec.push(junction);
122			}
123		}
124	}
125
126	// Reverse the vec to restore original order, then build Junctions
127	beneficiary_junctions_vec.reverse();
128	let beneficiary_junctions = junctions_from_slice(&beneficiary_junctions_vec)?;
129
130	if location.parent_count() == 1 {
131		Some((Location::parent(), beneficiary_junctions.into_location()))
132	} else {
133		None
134	}
135}
136
137#[cfg(test)]
138mod tests {
139	use super::*;
140	use xcm::latest::prelude::*;
141
142	#[test]
143	fn test_split_location_single_beneficiary_junction() {
144		// Test: Parachain(2) + AccountKey20
145		let location = Location {
146			parents: 1,
147			interior: [
148				Parachain(2),
149				AccountKey20 {
150					network: None,
151					key: [1u8; 20],
152				},
153			]
154			.into(),
155		};
156
157		let (chain_part, beneficiary) =
158			split_location_into_chain_part_and_beneficiary(location).unwrap();
159
160		// Chain part should be Parachain(2)
161		assert_eq!(
162			chain_part,
163			Location {
164				parents: 1,
165				interior: [Parachain(2)].into()
166			}
167		);
168
169		// Beneficiary should be AccountKey20
170		assert_eq!(
171			beneficiary,
172			Location {
173				parents: 0,
174				interior: [AccountKey20 {
175					network: None,
176					key: [1u8; 20]
177				}]
178				.into()
179			}
180		);
181	}
182
183	#[test]
184	fn test_split_location_multiple_beneficiary_junctions_order_preserved() {
185		// Test: Parachain(100) + AccountId32 + GeneralIndex(42)
186		// This test verifies that the order is preserved (AccountId32 comes before GeneralIndex)
187		let account_id = AccountId32 {
188			network: None,
189			id: [2u8; 32],
190		};
191		let general_index = GeneralIndex(42);
192
193		let location = Location {
194			parents: 1,
195			interior: [Parachain(100), account_id, general_index].into(),
196		};
197
198		let (chain_part, beneficiary) =
199			split_location_into_chain_part_and_beneficiary(location).unwrap();
200
201		// Chain part should be Parachain(100)
202		assert_eq!(
203			chain_part,
204			Location {
205				parents: 1,
206				interior: [Parachain(100)].into()
207			}
208		);
209
210		// Beneficiary should preserve order: AccountId32, then GeneralIndex
211		assert_eq!(
212			beneficiary,
213			Location {
214				parents: 0,
215				interior: [account_id, general_index].into()
216			}
217		);
218	}
219
220	#[test]
221	fn test_split_location_three_beneficiary_junctions_order_preserved() {
222		// Test: Parachain(200) + PalletInstance(5) + AccountId32 + GeneralIndex(10)
223		let pallet = PalletInstance(5);
224		let account_id = AccountId32 {
225			network: None,
226			id: [3u8; 32],
227		};
228		let general_index = GeneralIndex(10);
229
230		let location = Location {
231			parents: 1,
232			interior: [Parachain(200), pallet, account_id, general_index].into(),
233		};
234
235		let (chain_part, beneficiary) =
236			split_location_into_chain_part_and_beneficiary(location).unwrap();
237
238		// Chain part should be Parachain(200)
239		assert_eq!(
240			chain_part,
241			Location {
242				parents: 1,
243				interior: [Parachain(200)].into()
244			}
245		);
246
247		// Beneficiary should preserve order: PalletInstance, AccountId32, GeneralIndex
248		assert_eq!(
249			beneficiary,
250			Location {
251				parents: 0,
252				interior: [pallet, account_id, general_index].into()
253			}
254		);
255	}
256
257	#[test]
258	fn test_split_location_with_global_consensus() {
259		// Test: GlobalConsensus(Polkadot) + Parachain(1) + AccountId32
260		let account_id = AccountId32 {
261			network: None,
262			id: [4u8; 32],
263		};
264
265		let location = Location {
266			parents: 1,
267			interior: [
268				GlobalConsensus(NetworkId::Polkadot),
269				Parachain(1),
270				account_id,
271			]
272			.into(),
273		};
274
275		let (chain_part, beneficiary) =
276			split_location_into_chain_part_and_beneficiary(location).unwrap();
277
278		// Chain part should stop at Parachain(1) (last chain identifier when processing from end)
279		// Since Parachain(1) is encountered first when processing from the end, chain_part includes
280		// both GlobalConsensus and Parachain(1)
281		assert_eq!(
282			chain_part,
283			Location {
284				parents: 1,
285				interior: [GlobalConsensus(NetworkId::Polkadot), Parachain(1)].into()
286			}
287		);
288
289		// Beneficiary should be AccountId32
290		assert_eq!(
291			beneficiary,
292			Location {
293				parents: 0,
294				interior: [account_id].into()
295			}
296		);
297	}
298
299	#[test]
300	fn test_split_location_parent_only() {
301		// Test: Parent + AccountId32
302		let account_id = AccountId32 {
303			network: None,
304			id: [5u8; 32],
305		};
306
307		let location = Location {
308			parents: 1,
309			interior: [account_id].into(),
310		};
311
312		let (chain_part, beneficiary) =
313			split_location_into_chain_part_and_beneficiary(location).unwrap();
314
315		// Chain part should be parent
316		assert_eq!(chain_part, Location::parent());
317
318		// Beneficiary should be AccountId32
319		assert_eq!(
320			beneficiary,
321			Location {
322				parents: 0,
323				interior: [account_id].into()
324			}
325		);
326	}
327
328	#[test]
329	fn test_split_location_multiple_junctions_order_verification() {
330		// Test with multiple junctions to verify order is NOT reversed
331		// Original: [Parachain(300), JunctionA, JunctionB, JunctionC]
332		// Expected beneficiary: [JunctionA, JunctionB, JunctionC] (same order)
333		let junction_a = AccountKey20 {
334			network: None,
335			key: [10u8; 20],
336		};
337		let junction_b = AccountId32 {
338			network: None,
339			id: [20u8; 32],
340		};
341		let junction_c = GeneralIndex(30);
342
343		let location = Location {
344			parents: 1,
345			interior: [Parachain(300), junction_a, junction_b, junction_c].into(),
346		};
347
348		let (chain_part, beneficiary) =
349			split_location_into_chain_part_and_beneficiary(location).unwrap();
350
351		// Verify chain part
352		assert_eq!(
353			chain_part,
354			Location {
355				parents: 1,
356				interior: [Parachain(300)].into()
357			}
358		);
359
360		// Verify beneficiary order is preserved (A, B, C - not reversed)
361		let beneficiary_interior = beneficiary.interior;
362		match beneficiary_interior {
363			Junctions::X3(junctions) => {
364				assert_eq!(
365					junctions[0],
366					Junction::AccountKey20 {
367						network: None,
368						key: [10u8; 20]
369					}
370				);
371				assert_eq!(
372					junctions[1],
373					Junction::AccountId32 {
374						network: None,
375						id: [20u8; 32]
376					}
377				);
378				assert_eq!(junctions[2], Junction::GeneralIndex(30));
379			}
380			_ => panic!("Expected X3 junctions"),
381		}
382	}
383
384	#[test]
385	fn test_split_location_no_beneficiary() {
386		// Test: Only Parachain (no beneficiary junctions)
387		let location = Location {
388			parents: 1,
389			interior: [Parachain(400)].into(),
390		};
391
392		let (chain_part, beneficiary) =
393			split_location_into_chain_part_and_beneficiary(location).unwrap();
394
395		// Chain part should be Parachain(400)
396		assert_eq!(
397			chain_part,
398			Location {
399				parents: 1,
400				interior: [Parachain(400)].into()
401			}
402		);
403
404		// Beneficiary should be Here (empty)
405		assert_eq!(
406			beneficiary,
407			Location {
408				parents: 0,
409				interior: Junctions::Here
410			}
411		);
412	}
413
414	#[test]
415	fn test_split_location_invalid_no_chain_identifier() {
416		// Test: Only beneficiary junctions, no chain identifier (should return None)
417		let location = Location {
418			parents: 0,
419			interior: [AccountId32 {
420				network: None,
421				id: [6u8; 32],
422			}]
423			.into(),
424		};
425
426		let result = split_location_into_chain_part_and_beneficiary(location);
427		assert!(result.is_none());
428	}
429
430	#[test]
431	fn test_split_location_invalid_wrong_parent_count() {
432		// Test: Wrong parent count (not 1)
433		let location = Location {
434			parents: 2,
435			interior: [AccountId32 {
436				network: None,
437				id: [7u8; 32],
438			}]
439			.into(),
440		};
441
442		let result = split_location_into_chain_part_and_beneficiary(location);
443		assert!(result.is_none());
444	}
445}