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 fn encode_multilocation(&self, location: &MultiLocation) -> Result<subxt::dynamic::Value> {
424 let interior = self.encode_junctions(&location.interior)?;
428
429 Ok(subxt::dynamic::Value::named_composite([
430 (
431 "parents",
432 subxt::dynamic::Value::u128(location.parents as u128),
433 ),
434 ("interior", interior),
435 ]))
436 }
437
438 fn encode_junctions(&self, junctions: &[Junction]) -> Result<subxt::dynamic::Value> {
439 if junctions.is_empty() {
440 return Ok(subxt::dynamic::Value::unnamed_variant("Here", vec![]));
442 }
443
444 let encoded_junctions: Vec<subxt::dynamic::Value> = junctions
446 .iter()
447 .map(|j| self.encode_junction(j))
448 .collect::<Result<Vec<_>>>()?;
449
450 let variant_name = match junctions.len() {
452 1 => "X1",
453 2 => "X2",
454 3 => "X3",
455 4 => "X4",
456 5 => "X5",
457 6 => "X6",
458 7 => "X7",
459 8 => "X8",
460 _ => return Err(Error::Transaction("Too many junctions (max 8)".to_string())),
461 };
462
463 Ok(subxt::dynamic::Value::unnamed_variant(
464 variant_name,
465 encoded_junctions,
466 ))
467 }
468
469 fn encode_junction(&self, junction: &Junction) -> Result<subxt::dynamic::Value> {
470 match junction {
471 Junction::Parachain(id) => Ok(subxt::dynamic::Value::unnamed_variant(
472 "Parachain",
473 vec![subxt::dynamic::Value::u128(*id as u128)],
474 )),
475 Junction::AccountId32 { network, id } => {
476 let network_value = if let Some(_net) = network {
477 subxt::dynamic::Value::unnamed_variant("Some", vec![])
479 } else {
480 subxt::dynamic::Value::unnamed_variant("None", vec![])
481 };
482
483 Ok(subxt::dynamic::Value::unnamed_variant(
484 "AccountId32",
485 vec![network_value, subxt::dynamic::Value::from_bytes(id)],
486 ))
487 }
488 Junction::AccountId20 { network, key } => {
489 let network_value = if let Some(_net) = network {
490 subxt::dynamic::Value::unnamed_variant("Some", vec![])
491 } else {
492 subxt::dynamic::Value::unnamed_variant("None", vec![])
493 };
494
495 Ok(subxt::dynamic::Value::unnamed_variant(
496 "AccountId20",
497 vec![network_value, subxt::dynamic::Value::from_bytes(key)],
498 ))
499 }
500 Junction::GeneralIndex(index) => Ok(subxt::dynamic::Value::unnamed_variant(
501 "GeneralIndex",
502 vec![subxt::dynamic::Value::u128(*index)],
503 )),
504 Junction::GeneralKey { data } => Ok(subxt::dynamic::Value::unnamed_variant(
505 "GeneralKey",
506 vec![subxt::dynamic::Value::from_bytes(data)],
507 )),
508 Junction::PalletInstance(instance) => Ok(subxt::dynamic::Value::unnamed_variant(
509 "PalletInstance",
510 vec![subxt::dynamic::Value::u128(*instance as u128)],
511 )),
512 }
513 }
514
515 fn encode_assets(&self, assets: &[XcmAsset]) -> Result<subxt::dynamic::Value> {
516 let encoded_assets: Vec<subxt::dynamic::Value> = assets
517 .iter()
518 .map(|asset| {
519 let id_value = match &asset.id {
520 AssetId::Concrete(location) => {
521 let loc = self.encode_multilocation(location)?;
522 subxt::dynamic::Value::unnamed_variant("Concrete", vec![loc])
523 }
524 AssetId::Abstract(data) => subxt::dynamic::Value::unnamed_variant(
525 "Abstract",
526 vec![subxt::dynamic::Value::from_bytes(data)],
527 ),
528 };
529
530 let fun_value = match asset.fun {
531 Fungibility::Fungible(amount) => subxt::dynamic::Value::unnamed_variant(
532 "Fungible",
533 vec![subxt::dynamic::Value::u128(amount)],
534 ),
535 Fungibility::NonFungible(instance) => subxt::dynamic::Value::unnamed_variant(
536 "NonFungible",
537 vec![subxt::dynamic::Value::u128(instance)],
538 ),
539 };
540
541 Ok(subxt::dynamic::Value::named_composite([
542 ("id", id_value),
543 ("fun", fun_value),
544 ]))
545 })
546 .collect::<Result<Vec<_>>>()?;
547
548 Ok(subxt::dynamic::Value::unnamed_variant(
550 "V3",
551 vec![subxt::dynamic::Value::unnamed_composite(encoded_assets)],
552 ))
553 }
554
555 fn encode_weight_limit(&self) -> Result<subxt::dynamic::Value> {
556 match self.config.weight_limit {
557 WeightLimit::Unlimited => {
558 Ok(subxt::dynamic::Value::unnamed_variant("Unlimited", vec![]))
559 }
560 WeightLimit::Limited(weight) => Ok(subxt::dynamic::Value::unnamed_variant(
561 "Limited",
562 vec![subxt::dynamic::Value::u128(weight as u128)],
563 )),
564 }
565 }
566
567 async fn submit_xcm_call<Call>(&self, call: &Call, wallet: &Wallet) -> Result<String>
568 where
569 Call: subxt::tx::Payload,
570 {
571 debug!("Submitting XCM extrinsic");
572
573 let pair = wallet
574 .sr25519_pair()
575 .ok_or_else(|| Error::Transaction("Wallet does not have SR25519 key".to_string()))?;
576
577 let signer = Sr25519Signer::new(pair.clone());
578
579 let mut progress = self
580 .client
581 .tx()
582 .sign_and_submit_then_watch_default(call, &signer)
583 .await
584 .map_err(|e| Error::Transaction(format!("Failed to submit XCM transaction: {}", e)))?;
585
586 while let Some(event) = progress.next().await {
588 let event =
589 event.map_err(|e| Error::Transaction(format!("XCM transaction error: {}", e)))?;
590
591 if event.as_in_block().is_some() {
592 info!("XCM transaction included in block");
593 }
594
595 if let Some(finalized) = event.as_finalized() {
596 let tx_hash = format!("0x{}", hex::encode(finalized.extrinsic_hash()));
597 info!("XCM transaction finalized: {}", tx_hash);
598
599 finalized
600 .wait_for_success()
601 .await
602 .map_err(|e| Error::Transaction(format!("XCM transaction failed: {}", e)))?;
603
604 return Ok(tx_hash);
605 }
606 }
607
608 Err(Error::Transaction(
609 "XCM transaction stream ended without finalization".to_string(),
610 ))
611 }
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617
618 #[test]
619 fn test_multilocation_parent() {
620 let location = MultiLocation::parent();
621 assert_eq!(location.parents, 1);
622 assert!(location.interior.is_empty());
623 assert!(location.is_parent());
624 }
625
626 #[test]
627 fn test_multilocation_parachain() {
628 let location = MultiLocation::parachain(2000);
629 assert_eq!(location.parents, 1);
630 assert_eq!(location.parachain_id(), Some(2000));
631 assert!(location.is_parachain());
632 }
633
634 #[test]
635 fn test_multilocation_account() {
636 let account = [1u8; 32];
637 let location = MultiLocation::account(account);
638 assert_eq!(location.parents, 0);
639 assert_eq!(location.interior.len(), 1);
640 }
641
642 #[test]
643 fn test_xcm_asset_native() {
644 let asset = XcmAsset::native(1000);
645 assert!(matches!(asset.fun, Fungibility::Fungible(1000)));
646 }
647
648 #[test]
649 fn test_weight_limit_default() {
650 let limit = WeightLimit::default();
651 assert!(matches!(limit, WeightLimit::Limited(5_000_000_000)));
652 }
653
654 #[test]
655 fn test_xcm_config_default() {
656 let config = XcmConfig::default();
657 assert_eq!(config.version, XcmVersion::V3);
658 assert!(matches!(config.weight_limit, WeightLimit::Limited(_)));
659 }
660}