1use crate::{Error, Result, Sr25519Signer, Wallet};
34use subxt::{OnlineClient, PolkadotConfig};
35use tracing::{debug, info};
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum XcmVersion {
40 V2,
42 V3,
44 V4,
46}
47
48impl Default for XcmVersion {
49 fn default() -> Self {
50 Self::V3
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum XcmTransferType {
57 ReserveTransfer,
60
61 Teleport,
64
65 LimitedReserveTransfer,
67
68 LimitedTeleport,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct MultiLocation {
75 pub parents: u8,
77 pub interior: Vec<Junction>,
79}
80
81impl MultiLocation {
82 pub fn parent() -> Self {
84 Self {
85 parents: 1,
86 interior: vec![],
87 }
88 }
89
90 pub fn parachain(para_id: u32) -> Self {
92 Self {
93 parents: 1,
94 interior: vec![Junction::Parachain(para_id)],
95 }
96 }
97
98 pub fn account(account_id: [u8; 32]) -> Self {
100 Self {
101 parents: 0,
102 interior: vec![Junction::AccountId32 {
103 network: None,
104 id: account_id,
105 }],
106 }
107 }
108
109 pub fn parachain_account(para_id: u32, account_id: [u8; 32]) -> Self {
111 Self {
112 parents: 1,
113 interior: vec![
114 Junction::Parachain(para_id),
115 Junction::AccountId32 {
116 network: None,
117 id: account_id,
118 },
119 ],
120 }
121 }
122
123 pub fn new(parents: u8, interior: Vec<Junction>) -> Self {
125 Self { parents, interior }
126 }
127
128 pub fn is_parent(&self) -> bool {
130 self.parents == 1 && self.interior.is_empty()
131 }
132
133 pub fn is_parachain(&self) -> bool {
135 matches!(self.interior.first(), Some(Junction::Parachain(_)))
136 }
137
138 pub fn parachain_id(&self) -> Option<u32> {
140 self.interior.first().and_then(|j| {
141 if let Junction::Parachain(id) = j {
142 Some(*id)
143 } else {
144 None
145 }
146 })
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum Junction {
153 Parachain(u32),
155
156 AccountId32 {
158 network: Option<NetworkId>,
159 id: [u8; 32],
160 },
161
162 AccountId20 {
164 network: Option<NetworkId>,
165 key: [u8; 20],
166 },
167
168 GeneralIndex(u128),
170
171 GeneralKey { data: Vec<u8> },
173
174 PalletInstance(u8),
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum NetworkId {
181 Polkadot,
183 Kusama,
185 Westend,
187 Rococo,
189 ByGenesis([u8; 32]),
191}
192
193#[derive(Debug, Clone)]
195pub struct XcmAsset {
196 pub id: AssetId,
198 pub fun: Fungibility,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
204pub enum AssetId {
205 Concrete(MultiLocation),
207 Abstract(Vec<u8>),
209}
210
211#[derive(Debug, Clone, PartialEq, Eq)]
213pub enum Fungibility {
214 Fungible(u128),
216 NonFungible(u128),
218}
219
220impl XcmAsset {
221 pub fn fungible(id: AssetId, amount: u128) -> Self {
223 Self {
224 id,
225 fun: Fungibility::Fungible(amount),
226 }
227 }
228
229 pub fn non_fungible(id: AssetId, instance: u128) -> Self {
231 Self {
232 id,
233 fun: Fungibility::NonFungible(instance),
234 }
235 }
236
237 pub fn native(amount: u128) -> Self {
239 Self::fungible(AssetId::Concrete(MultiLocation::new(0, vec![])), amount)
240 }
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub enum WeightLimit {
246 Unlimited,
248 Limited(u64),
250}
251
252impl Default for WeightLimit {
253 fn default() -> Self {
254 Self::Limited(5_000_000_000)
256 }
257}
258
259#[derive(Debug, Clone, Default)]
261pub struct XcmConfig {
262 #[allow(clippy::derivable_impls)]
264 pub version: XcmVersion,
265 pub weight_limit: WeightLimit,
267 pub fee_asset: Option<XcmAsset>,
269}
270
271pub struct XcmExecutor {
273 client: OnlineClient<PolkadotConfig>,
274 config: XcmConfig,
275}
276
277impl XcmExecutor {
278 pub fn new(client: OnlineClient<PolkadotConfig>) -> Self {
280 Self {
281 client,
282 config: XcmConfig::default(),
283 }
284 }
285
286 pub fn with_config(client: OnlineClient<PolkadotConfig>, config: XcmConfig) -> Self {
288 Self { client, config }
289 }
290
291 pub fn with_version(mut self, version: XcmVersion) -> Self {
293 self.config.version = version;
294 self
295 }
296
297 pub fn with_weight_limit(mut self, limit: WeightLimit) -> Self {
299 self.config.weight_limit = limit;
300 self
301 }
302
303 pub async fn reserve_transfer(
316 &self,
317 wallet: &Wallet,
318 dest: MultiLocation,
319 beneficiary: [u8; 32],
320 assets: Vec<XcmAsset>,
321 ) -> Result<String> {
322 info!("Executing reserve transfer to {:?} for beneficiary", dest);
323
324 let dest_value = self.encode_multilocation(&dest)?;
326 let beneficiary_value = self.encode_multilocation(&MultiLocation::account(beneficiary))?;
327 let assets_value = self.encode_assets(&assets)?;
328 let fee_index = 0u32; let call = subxt::dynamic::tx(
331 "XcmPallet",
332 "limited_reserve_transfer_assets",
333 vec![
334 dest_value,
335 beneficiary_value,
336 assets_value,
337 subxt::dynamic::Value::u128(fee_index as u128),
338 self.encode_weight_limit()?,
339 ],
340 );
341
342 self.submit_xcm_call(&call, wallet).await
343 }
344
345 pub async fn teleport(
358 &self,
359 wallet: &Wallet,
360 dest: MultiLocation,
361 beneficiary: [u8; 32],
362 assets: Vec<XcmAsset>,
363 ) -> Result<String> {
364 info!("Executing teleport to {:?} for beneficiary", dest);
365
366 let dest_value = self.encode_multilocation(&dest)?;
367 let beneficiary_value = self.encode_multilocation(&MultiLocation::account(beneficiary))?;
368 let assets_value = self.encode_assets(&assets)?;
369 let fee_index = 0u32;
370
371 let call = subxt::dynamic::tx(
372 "XcmPallet",
373 "limited_teleport_assets",
374 vec![
375 dest_value,
376 beneficiary_value,
377 assets_value,
378 subxt::dynamic::Value::u128(fee_index as u128),
379 self.encode_weight_limit()?,
380 ],
381 );
382
383 self.submit_xcm_call(&call, wallet).await
384 }
385
386 pub async fn transfer_to_relay(
390 &self,
391 wallet: &Wallet,
392 beneficiary: [u8; 32],
393 amount: u128,
394 ) -> Result<String> {
395 debug!("Transferring {} to relay chain", amount);
396
397 self.reserve_transfer(
398 wallet,
399 MultiLocation::parent(),
400 beneficiary,
401 vec![XcmAsset::native(amount)],
402 )
403 .await
404 }
405
406 pub async fn transfer_to_parachain(
410 &self,
411 wallet: &Wallet,
412 para_id: u32,
413 beneficiary: [u8; 32],
414 amount: u128,
415 ) -> Result<String> {
416 debug!("Transferring {} to parachain {}", amount, para_id);
417
418 self.reserve_transfer(
419 wallet,
420 MultiLocation::parachain(para_id),
421 beneficiary,
422 vec![XcmAsset::native(amount)],
423 )
424 .await
425 }
426
427 #[allow(clippy::result_large_err)]
430 fn encode_multilocation(&self, location: &MultiLocation) -> Result<subxt::dynamic::Value> {
431 let interior = self.encode_junctions(&location.interior)?;
435
436 Ok(subxt::dynamic::Value::named_composite([
437 (
438 "parents",
439 subxt::dynamic::Value::u128(location.parents as u128),
440 ),
441 ("interior", interior),
442 ]))
443 }
444
445 #[allow(clippy::result_large_err)]
446 fn encode_junctions(&self, junctions: &[Junction]) -> Result<subxt::dynamic::Value> {
447 if junctions.is_empty() {
448 return Ok(subxt::dynamic::Value::unnamed_variant("Here", vec![]));
450 }
451
452 let encoded_junctions: Vec<subxt::dynamic::Value> = junctions
454 .iter()
455 .map(|j| self.encode_junction(j))
456 .collect::<Result<Vec<_>>>()?;
457
458 let variant_name = match junctions.len() {
460 1 => "X1",
461 2 => "X2",
462 3 => "X3",
463 4 => "X4",
464 5 => "X5",
465 6 => "X6",
466 7 => "X7",
467 8 => "X8",
468 _ => return Err(Error::Transaction("Too many junctions (max 8)".to_string())),
469 };
470
471 Ok(subxt::dynamic::Value::unnamed_variant(
472 variant_name,
473 encoded_junctions,
474 ))
475 }
476
477 #[allow(clippy::result_large_err)]
478 fn encode_junction(&self, junction: &Junction) -> Result<subxt::dynamic::Value> {
479 match junction {
480 Junction::Parachain(id) => Ok(subxt::dynamic::Value::unnamed_variant(
481 "Parachain",
482 vec![subxt::dynamic::Value::u128(*id as u128)],
483 )),
484 Junction::AccountId32 { network, id } => {
485 let network_value = if let Some(_net) = network {
486 subxt::dynamic::Value::unnamed_variant("Some", vec![])
488 } else {
489 subxt::dynamic::Value::unnamed_variant("None", vec![])
490 };
491
492 Ok(subxt::dynamic::Value::unnamed_variant(
493 "AccountId32",
494 vec![network_value, subxt::dynamic::Value::from_bytes(id)],
495 ))
496 }
497 Junction::AccountId20 { network, key } => {
498 let network_value = if let Some(_net) = network {
499 subxt::dynamic::Value::unnamed_variant("Some", vec![])
500 } else {
501 subxt::dynamic::Value::unnamed_variant("None", vec![])
502 };
503
504 Ok(subxt::dynamic::Value::unnamed_variant(
505 "AccountId20",
506 vec![network_value, subxt::dynamic::Value::from_bytes(key)],
507 ))
508 }
509 Junction::GeneralIndex(index) => Ok(subxt::dynamic::Value::unnamed_variant(
510 "GeneralIndex",
511 vec![subxt::dynamic::Value::u128(*index)],
512 )),
513 Junction::GeneralKey { data } => Ok(subxt::dynamic::Value::unnamed_variant(
514 "GeneralKey",
515 vec![subxt::dynamic::Value::from_bytes(data)],
516 )),
517 Junction::PalletInstance(instance) => Ok(subxt::dynamic::Value::unnamed_variant(
518 "PalletInstance",
519 vec![subxt::dynamic::Value::u128(*instance as u128)],
520 )),
521 }
522 }
523
524 #[allow(clippy::result_large_err)]
525 fn encode_assets(&self, assets: &[XcmAsset]) -> Result<subxt::dynamic::Value> {
526 let encoded_assets: Vec<subxt::dynamic::Value> = assets
527 .iter()
528 .map(|asset| {
529 let id_value = match &asset.id {
530 AssetId::Concrete(location) => {
531 let loc = self.encode_multilocation(location)?;
532 subxt::dynamic::Value::unnamed_variant("Concrete", vec![loc])
533 }
534 AssetId::Abstract(data) => subxt::dynamic::Value::unnamed_variant(
535 "Abstract",
536 vec![subxt::dynamic::Value::from_bytes(data)],
537 ),
538 };
539
540 let fun_value = match asset.fun {
541 Fungibility::Fungible(amount) => subxt::dynamic::Value::unnamed_variant(
542 "Fungible",
543 vec![subxt::dynamic::Value::u128(amount)],
544 ),
545 Fungibility::NonFungible(instance) => subxt::dynamic::Value::unnamed_variant(
546 "NonFungible",
547 vec![subxt::dynamic::Value::u128(instance)],
548 ),
549 };
550
551 Ok(subxt::dynamic::Value::named_composite([
552 ("id", id_value),
553 ("fun", fun_value),
554 ]))
555 })
556 .collect::<Result<Vec<_>>>()?;
557
558 Ok(subxt::dynamic::Value::unnamed_variant(
560 "V3",
561 vec![subxt::dynamic::Value::unnamed_composite(encoded_assets)],
562 ))
563 }
564
565 #[allow(clippy::result_large_err)]
566 fn encode_weight_limit(&self) -> Result<subxt::dynamic::Value> {
567 match self.config.weight_limit {
568 WeightLimit::Unlimited => {
569 Ok(subxt::dynamic::Value::unnamed_variant("Unlimited", vec![]))
570 }
571 WeightLimit::Limited(weight) => Ok(subxt::dynamic::Value::unnamed_variant(
572 "Limited",
573 vec![subxt::dynamic::Value::u128(weight as u128)],
574 )),
575 }
576 }
577
578 async fn submit_xcm_call<Call>(&self, call: &Call, wallet: &Wallet) -> Result<String>
579 where
580 Call: subxt::tx::Payload,
581 {
582 debug!("Submitting XCM extrinsic");
583
584 let pair = wallet
585 .sr25519_pair()
586 .ok_or_else(|| Error::Transaction("Wallet does not have SR25519 key".to_string()))?;
587
588 let signer = Sr25519Signer::new(pair.clone());
589
590 let mut progress = self
591 .client
592 .tx()
593 .sign_and_submit_then_watch_default(call, &signer)
594 .await
595 .map_err(|e| Error::Transaction(format!("Failed to submit XCM transaction: {}", e)))?;
596
597 while let Some(event) = progress.next().await {
599 let event =
600 event.map_err(|e| Error::Transaction(format!("XCM transaction error: {}", e)))?;
601
602 if event.as_in_block().is_some() {
603 info!("XCM transaction included in block");
604 }
605
606 if let Some(finalized) = event.as_finalized() {
607 let tx_hash = format!("0x{}", hex::encode(finalized.extrinsic_hash()));
608 info!("XCM transaction finalized: {}", tx_hash);
609
610 finalized
611 .wait_for_success()
612 .await
613 .map_err(|e| Error::Transaction(format!("XCM transaction failed: {}", e)))?;
614
615 return Ok(tx_hash);
616 }
617 }
618
619 Err(Error::Transaction(
620 "XCM transaction stream ended without finalization".to_string(),
621 ))
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628
629 #[test]
630 fn test_multilocation_parent() {
631 let location = MultiLocation::parent();
632 assert_eq!(location.parents, 1);
633 assert!(location.interior.is_empty());
634 assert!(location.is_parent());
635 }
636
637 #[test]
638 fn test_multilocation_parachain() {
639 let location = MultiLocation::parachain(2000);
640 assert_eq!(location.parents, 1);
641 assert_eq!(location.parachain_id(), Some(2000));
642 assert!(location.is_parachain());
643 }
644
645 #[test]
646 fn test_multilocation_account() {
647 let account = [1u8; 32];
648 let location = MultiLocation::account(account);
649 assert_eq!(location.parents, 0);
650 assert_eq!(location.interior.len(), 1);
651 }
652
653 #[test]
654 fn test_xcm_asset_native() {
655 let asset = XcmAsset::native(1000);
656 assert!(matches!(asset.fun, Fungibility::Fungible(1000)));
657 }
658
659 #[test]
660 fn test_weight_limit_default() {
661 let limit = WeightLimit::default();
662 assert!(matches!(limit, WeightLimit::Limited(5_000_000_000)));
663 }
664
665 #[test]
666 fn test_xcm_config_default() {
667 let config = XcmConfig::default();
668 assert_eq!(config.version, XcmVersion::V3);
669 assert!(matches!(config.weight_limit, WeightLimit::Limited(_)));
670 }
671}