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