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 #[allow(clippy::derivable_impls)]
259 pub version: XcmVersion,
260 pub weight_limit: WeightLimit,
262 pub fee_asset: Option<XcmAsset>,
264}
265
266pub struct XcmExecutor {
268 client: OnlineClient<PolkadotConfig>,
269 config: XcmConfig,
270}
271
272impl XcmExecutor {
273 pub fn new(client: OnlineClient<PolkadotConfig>) -> Self {
275 Self {
276 client,
277 config: XcmConfig::default(),
278 }
279 }
280
281 pub fn with_config(client: OnlineClient<PolkadotConfig>, config: XcmConfig) -> Self {
283 Self { client, config }
284 }
285
286 pub fn with_version(mut self, version: XcmVersion) -> Self {
288 self.config.version = version;
289 self
290 }
291
292 pub fn with_weight_limit(mut self, limit: WeightLimit) -> Self {
294 self.config.weight_limit = limit;
295 self
296 }
297
298 pub async fn reserve_transfer(
311 &self,
312 wallet: &Wallet,
313 dest: MultiLocation,
314 beneficiary: [u8; 32],
315 assets: Vec<XcmAsset>,
316 ) -> Result<String> {
317 info!("Executing reserve transfer to {:?} for beneficiary", dest);
318
319 let dest_value = self.encode_multilocation(&dest)?;
321 let beneficiary_value = self.encode_multilocation(&MultiLocation::account(beneficiary))?;
322 let assets_value = self.encode_assets(&assets)?;
323 let fee_index = 0u32; let call = subxt::dynamic::tx(
326 "XcmPallet",
327 "limited_reserve_transfer_assets",
328 vec![
329 dest_value,
330 beneficiary_value,
331 assets_value,
332 subxt::dynamic::Value::u128(fee_index as u128),
333 self.encode_weight_limit()?,
334 ],
335 );
336
337 self.submit_xcm_call(&call, wallet).await
338 }
339
340 pub async fn teleport(
353 &self,
354 wallet: &Wallet,
355 dest: MultiLocation,
356 beneficiary: [u8; 32],
357 assets: Vec<XcmAsset>,
358 ) -> Result<String> {
359 info!("Executing teleport to {:?} for beneficiary", dest);
360
361 let dest_value = self.encode_multilocation(&dest)?;
362 let beneficiary_value = self.encode_multilocation(&MultiLocation::account(beneficiary))?;
363 let assets_value = self.encode_assets(&assets)?;
364 let fee_index = 0u32;
365
366 let call = subxt::dynamic::tx(
367 "XcmPallet",
368 "limited_teleport_assets",
369 vec![
370 dest_value,
371 beneficiary_value,
372 assets_value,
373 subxt::dynamic::Value::u128(fee_index as u128),
374 self.encode_weight_limit()?,
375 ],
376 );
377
378 self.submit_xcm_call(&call, wallet).await
379 }
380
381 pub async fn transfer_to_relay(
385 &self,
386 wallet: &Wallet,
387 beneficiary: [u8; 32],
388 amount: u128,
389 ) -> Result<String> {
390 debug!("Transferring {} to relay chain", amount);
391
392 self.reserve_transfer(
393 wallet,
394 MultiLocation::parent(),
395 beneficiary,
396 vec![XcmAsset::native(amount)],
397 )
398 .await
399 }
400
401 pub async fn transfer_to_parachain(
405 &self,
406 wallet: &Wallet,
407 para_id: u32,
408 beneficiary: [u8; 32],
409 amount: u128,
410 ) -> Result<String> {
411 debug!("Transferring {} to parachain {}", amount, para_id);
412
413 self.reserve_transfer(
414 wallet,
415 MultiLocation::parachain(para_id),
416 beneficiary,
417 vec![XcmAsset::native(amount)],
418 )
419 .await
420 }
421
422 #[allow(clippy::result_large_err)]
425 fn encode_multilocation(&self, location: &MultiLocation) -> Result<subxt::dynamic::Value> {
426 let interior = self.encode_junctions(&location.interior)?;
430
431 Ok(subxt::dynamic::Value::named_composite([
432 (
433 "parents",
434 subxt::dynamic::Value::u128(location.parents as u128),
435 ),
436 ("interior", interior),
437 ]))
438 }
439
440 #[allow(clippy::result_large_err)]
441 fn encode_junctions(&self, junctions: &[Junction]) -> Result<subxt::dynamic::Value> {
442 if junctions.is_empty() {
443 return Ok(subxt::dynamic::Value::unnamed_variant("Here", vec![]));
445 }
446
447 let encoded_junctions: Vec<subxt::dynamic::Value> = junctions
449 .iter()
450 .map(|j| self.encode_junction(j))
451 .collect::<Result<Vec<_>>>()?;
452
453 let variant_name = match junctions.len() {
455 1 => "X1",
456 2 => "X2",
457 3 => "X3",
458 4 => "X4",
459 5 => "X5",
460 6 => "X6",
461 7 => "X7",
462 8 => "X8",
463 _ => return Err(Error::Transaction("Too many junctions (max 8)".to_string())),
464 };
465
466 Ok(subxt::dynamic::Value::unnamed_variant(
467 variant_name,
468 encoded_junctions,
469 ))
470 }
471
472 #[allow(clippy::result_large_err)]
473 fn encode_junction(&self, junction: &Junction) -> Result<subxt::dynamic::Value> {
474 match junction {
475 Junction::Parachain(id) => Ok(subxt::dynamic::Value::unnamed_variant(
476 "Parachain",
477 vec![subxt::dynamic::Value::u128(*id as u128)],
478 )),
479 Junction::AccountId32 { network, id } => {
480 let network_value = if let Some(_net) = network {
481 subxt::dynamic::Value::unnamed_variant("Some", vec![])
483 } else {
484 subxt::dynamic::Value::unnamed_variant("None", vec![])
485 };
486
487 Ok(subxt::dynamic::Value::unnamed_variant(
488 "AccountId32",
489 vec![network_value, subxt::dynamic::Value::from_bytes(id)],
490 ))
491 }
492 Junction::AccountId20 { network, key } => {
493 let network_value = if let Some(_net) = network {
494 subxt::dynamic::Value::unnamed_variant("Some", vec![])
495 } else {
496 subxt::dynamic::Value::unnamed_variant("None", vec![])
497 };
498
499 Ok(subxt::dynamic::Value::unnamed_variant(
500 "AccountId20",
501 vec![network_value, subxt::dynamic::Value::from_bytes(key)],
502 ))
503 }
504 Junction::GeneralIndex(index) => Ok(subxt::dynamic::Value::unnamed_variant(
505 "GeneralIndex",
506 vec![subxt::dynamic::Value::u128(*index)],
507 )),
508 Junction::GeneralKey { data } => Ok(subxt::dynamic::Value::unnamed_variant(
509 "GeneralKey",
510 vec![subxt::dynamic::Value::from_bytes(data)],
511 )),
512 Junction::PalletInstance(instance) => Ok(subxt::dynamic::Value::unnamed_variant(
513 "PalletInstance",
514 vec![subxt::dynamic::Value::u128(*instance as u128)],
515 )),
516 }
517 }
518
519 #[allow(clippy::result_large_err)]
520 fn encode_assets(&self, assets: &[XcmAsset]) -> Result<subxt::dynamic::Value> {
521 let encoded_assets: Vec<subxt::dynamic::Value> = assets
522 .iter()
523 .map(|asset| {
524 let id_value = match &asset.id {
525 AssetId::Concrete(location) => {
526 let loc = self.encode_multilocation(location)?;
527 subxt::dynamic::Value::unnamed_variant("Concrete", vec![loc])
528 }
529 AssetId::Abstract(data) => subxt::dynamic::Value::unnamed_variant(
530 "Abstract",
531 vec![subxt::dynamic::Value::from_bytes(data)],
532 ),
533 };
534
535 let fun_value = match asset.fun {
536 Fungibility::Fungible(amount) => subxt::dynamic::Value::unnamed_variant(
537 "Fungible",
538 vec![subxt::dynamic::Value::u128(amount)],
539 ),
540 Fungibility::NonFungible(instance) => subxt::dynamic::Value::unnamed_variant(
541 "NonFungible",
542 vec![subxt::dynamic::Value::u128(instance)],
543 ),
544 };
545
546 Ok(subxt::dynamic::Value::named_composite([
547 ("id", id_value),
548 ("fun", fun_value),
549 ]))
550 })
551 .collect::<Result<Vec<_>>>()?;
552
553 Ok(subxt::dynamic::Value::unnamed_variant(
555 "V3",
556 vec![subxt::dynamic::Value::unnamed_composite(encoded_assets)],
557 ))
558 }
559
560 #[allow(clippy::result_large_err)]
561 fn encode_weight_limit(&self) -> Result<subxt::dynamic::Value> {
562 match self.config.weight_limit {
563 WeightLimit::Unlimited => {
564 Ok(subxt::dynamic::Value::unnamed_variant("Unlimited", vec![]))
565 }
566 WeightLimit::Limited(weight) => Ok(subxt::dynamic::Value::unnamed_variant(
567 "Limited",
568 vec![subxt::dynamic::Value::u128(weight as u128)],
569 )),
570 }
571 }
572
573 async fn submit_xcm_call<Call>(&self, call: &Call, wallet: &Wallet) -> Result<String>
574 where
575 Call: subxt::tx::Payload,
576 {
577 debug!("Submitting XCM extrinsic");
578
579 let pair = wallet
580 .sr25519_pair()
581 .ok_or_else(|| Error::Transaction("Wallet does not have SR25519 key".to_string()))?;
582
583 let signer = Sr25519Signer::new(pair.clone());
584
585 let mut progress = self
586 .client
587 .tx()
588 .sign_and_submit_then_watch_default(call, &signer)
589 .await
590 .map_err(|e| Error::Transaction(format!("Failed to submit XCM transaction: {}", e)))?;
591
592 while let Some(event) = progress.next().await {
594 let event =
595 event.map_err(|e| Error::Transaction(format!("XCM transaction error: {}", e)))?;
596
597 if event.as_in_block().is_some() {
598 info!("XCM transaction included in block");
599 }
600
601 if let Some(finalized) = event.as_finalized() {
602 let tx_hash = format!("0x{}", hex::encode(finalized.extrinsic_hash()));
603 info!("XCM transaction finalized: {}", tx_hash);
604
605 finalized
606 .wait_for_success()
607 .await
608 .map_err(|e| Error::Transaction(format!("XCM transaction failed: {}", e)))?;
609
610 return Ok(tx_hash);
611 }
612 }
613
614 Err(Error::Transaction(
615 "XCM transaction stream ended without finalization".to_string(),
616 ))
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623
624 #[test]
625 fn test_multilocation_parent() {
626 let location = MultiLocation::parent();
627 assert_eq!(location.parents, 1);
628 assert!(location.interior.is_empty());
629 assert!(location.is_parent());
630 }
631
632 #[test]
633 fn test_multilocation_parachain() {
634 let location = MultiLocation::parachain(2000);
635 assert_eq!(location.parents, 1);
636 assert_eq!(location.parachain_id(), Some(2000));
637 assert!(location.is_parachain());
638 }
639
640 #[test]
641 fn test_multilocation_account() {
642 let account = [1u8; 32];
643 let location = MultiLocation::account(account);
644 assert_eq!(location.parents, 0);
645 assert_eq!(location.interior.len(), 1);
646 }
647
648 #[test]
649 fn test_xcm_asset_native() {
650 let asset = XcmAsset::native(1000);
651 assert!(matches!(asset.fun, Fungibility::Fungible(1000)));
652 }
653
654 #[test]
655 fn test_weight_limit_default() {
656 let limit = WeightLimit::default();
657 assert!(matches!(limit, WeightLimit::Limited(5_000_000_000)));
658 }
659
660 #[test]
661 fn test_xcm_config_default() {
662 let config = XcmConfig::default();
663 assert_eq!(config.version, XcmVersion::V3);
664 assert!(matches!(config.weight_limit, WeightLimit::Limited(_)));
665 }
666}