apex_sdk_substrate/
xcm.rs

1//! XCM (Cross-Consensus Messaging) support for cross-chain transfers
2//!
3//! This module provides functionality for sending XCM messages across parachains
4//! and relay chains in the Polkadot ecosystem.
5//!
6//! ## Features
7//!
8//! - Reserve transfers (transfer assets via reserve chain)
9//! - Teleport transfers (burn and mint across chains)
10//! - Multi-location address handling
11//! - XCM v3/v4 support
12//! - Parachain-to-parachain transfers
13//! - Parachain-to-relay transfers
14//!
15//! ## Example
16//!
17//! ```rust,ignore
18//! use apex_sdk_substrate::xcm::{XcmExecutor, XcmTransferType, MultiLocation};
19//!
20//! let executor = XcmExecutor::new(client);
21//!
22//! // Transfer from parachain to relay chain
23//! let tx_hash = executor
24//!     .transfer(
25//!         beneficiary,
26//!         amount,
27//!         XcmTransferType::ReserveTransfer,
28//!         MultiLocation::parent(),
29//!     )
30//!     .await?;
31//! ```
32
33use crate::{Error, Result, Sr25519Signer, Wallet};
34use subxt::{OnlineClient, PolkadotConfig};
35use tracing::{debug, info};
36
37/// XCM version to use for message construction
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum XcmVersion {
40    /// XCM version 2
41    V2,
42    /// XCM version 3 (recommended)
43    #[default]
44    V3,
45    /// XCM version 4 (latest)
46    V4,
47}
48
49/// Type of XCM transfer to perform
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum XcmTransferType {
52    /// Reserve transfer - assets are held in reserve on the origin chain
53    /// Suitable for transfers to parachains that trust the origin
54    ReserveTransfer,
55
56    /// Teleport - assets are burned on origin and minted on destination
57    /// Requires mutual trust between chains
58    Teleport,
59
60    /// Limited reserve transfer - like reserve transfer but with weight limits
61    LimitedReserveTransfer,
62
63    /// Limited teleport - like teleport but with weight limits
64    LimitedTeleport,
65}
66
67/// Multi-location representation for XCM addressing
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct MultiLocation {
70    /// Number of parent levels to traverse
71    pub parents: u8,
72    /// Interior junctions (e.g., parachain ID, account ID)
73    pub interior: Vec<Junction>,
74}
75
76impl MultiLocation {
77    /// Create a MultiLocation pointing to the parent (relay chain)
78    pub fn parent() -> Self {
79        Self {
80            parents: 1,
81            interior: vec![],
82        }
83    }
84
85    /// Create a MultiLocation for a specific parachain
86    pub fn parachain(para_id: u32) -> Self {
87        Self {
88            parents: 1,
89            interior: vec![Junction::Parachain(para_id)],
90        }
91    }
92
93    /// Create a MultiLocation for an account on the current chain
94    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    /// Create a MultiLocation for an account on a specific parachain
105    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    /// Create a MultiLocation from raw parts
119    pub fn new(parents: u8, interior: Vec<Junction>) -> Self {
120        Self { parents, interior }
121    }
122
123    /// Check if this location points to the parent chain
124    pub fn is_parent(&self) -> bool {
125        self.parents == 1 && self.interior.is_empty()
126    }
127
128    /// Check if this location points to a parachain
129    pub fn is_parachain(&self) -> bool {
130        matches!(self.interior.first(), Some(Junction::Parachain(_)))
131    }
132
133    /// Get the parachain ID if this location points to a parachain
134    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/// Interior junction types for multi-location
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub enum Junction {
148    /// Parachain junction with parachain ID
149    Parachain(u32),
150
151    /// AccountId32 junction
152    AccountId32 {
153        network: Option<NetworkId>,
154        id: [u8; 32],
155    },
156
157    /// AccountId20 junction (for EVM accounts)
158    AccountId20 {
159        network: Option<NetworkId>,
160        key: [u8; 20],
161    },
162
163    /// General index junction
164    GeneralIndex(u128),
165
166    /// General key junction
167    GeneralKey { data: Vec<u8> },
168
169    /// Pallet instance junction
170    PalletInstance(u8),
171}
172
173/// Network identifier for cross-consensus messaging
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum NetworkId {
176    /// Polkadot relay chain
177    Polkadot,
178    /// Kusama relay chain
179    Kusama,
180    /// Westend test network
181    Westend,
182    /// Rococo test network
183    Rococo,
184    /// Generic network by ID
185    ByGenesis([u8; 32]),
186}
187
188/// XCM asset representation
189#[derive(Debug, Clone)]
190pub struct XcmAsset {
191    /// Asset identifier (multi-location)
192    pub id: AssetId,
193    /// Fungibility (amount or instance)
194    pub fun: Fungibility,
195}
196
197/// Asset identifier for XCM
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub enum AssetId {
200    /// Concrete asset identified by multi-location
201    Concrete(MultiLocation),
202    /// Abstract asset identified by bytes
203    Abstract(Vec<u8>),
204}
205
206/// Fungibility of an asset
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub enum Fungibility {
209    /// Fungible asset with amount
210    Fungible(u128),
211    /// Non-fungible asset with instance ID
212    NonFungible(u128),
213}
214
215impl XcmAsset {
216    /// Create a fungible asset (e.g., tokens)
217    pub fn fungible(id: AssetId, amount: u128) -> Self {
218        Self {
219            id,
220            fun: Fungibility::Fungible(amount),
221        }
222    }
223
224    /// Create a non-fungible asset (e.g., NFT)
225    pub fn non_fungible(id: AssetId, instance: u128) -> Self {
226        Self {
227            id,
228            fun: Fungibility::NonFungible(instance),
229        }
230    }
231
232    /// Create a native token asset for the current chain
233    pub fn native(amount: u128) -> Self {
234        Self::fungible(AssetId::Concrete(MultiLocation::new(0, vec![])), amount)
235    }
236}
237
238/// Weight limit for XCM execution
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub enum WeightLimit {
241    /// Unlimited weight (dangerous, use with caution)
242    Unlimited,
243    /// Limited to specific weight units
244    Limited(u64),
245}
246
247impl Default for WeightLimit {
248    fn default() -> Self {
249        // Default to a conservative weight limit (5 billion units)
250        Self::Limited(5_000_000_000)
251    }
252}
253
254/// Configuration for XCM transfers
255#[derive(Debug, Clone, Default)]
256pub struct XcmConfig {
257    /// XCM version to use
258    pub version: XcmVersion,
259    /// Weight limit for execution
260    pub weight_limit: WeightLimit,
261    /// Fee asset to use (defaults to native token)
262    pub fee_asset: Option<XcmAsset>,
263}
264
265/// XCM executor for sending cross-chain messages
266pub struct XcmExecutor {
267    client: OnlineClient<PolkadotConfig>,
268    config: XcmConfig,
269}
270
271impl XcmExecutor {
272    /// Create a new XCM executor
273    pub fn new(client: OnlineClient<PolkadotConfig>) -> Self {
274        Self {
275            client,
276            config: XcmConfig::default(),
277        }
278    }
279
280    /// Create a new XCM executor with custom configuration
281    pub fn with_config(client: OnlineClient<PolkadotConfig>, config: XcmConfig) -> Self {
282        Self { client, config }
283    }
284
285    /// Set the XCM version
286    pub fn with_version(mut self, version: XcmVersion) -> Self {
287        self.config.version = version;
288        self
289    }
290
291    /// Set the weight limit
292    pub fn with_weight_limit(mut self, limit: WeightLimit) -> Self {
293        self.config.weight_limit = limit;
294        self
295    }
296
297    /// Execute a reserve transfer to another chain
298    ///
299    /// # Arguments
300    ///
301    /// * `wallet` - Wallet to sign the transaction
302    /// * `dest` - Destination multi-location (parachain or relay chain)
303    /// * `beneficiary` - Beneficiary account on the destination chain
304    /// * `assets` - Assets to transfer
305    ///
306    /// # Returns
307    ///
308    /// Transaction hash of the XCM transfer extrinsic
309    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        // Build the reserve transfer call using dynamic API
319        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; // Use first asset for fees
323
324        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    /// Execute a teleport transfer to another chain
340    ///
341    /// # Arguments
342    ///
343    /// * `wallet` - Wallet to sign the transaction
344    /// * `dest` - Destination multi-location
345    /// * `beneficiary` - Beneficiary account on the destination chain
346    /// * `assets` - Assets to transfer
347    ///
348    /// # Returns
349    ///
350    /// Transaction hash of the XCM transfer extrinsic
351    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    /// Transfer to relay chain (convenience method)
381    ///
382    /// Automatically uses reserve transfer to parent chain
383    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    /// Transfer to another parachain
401    ///
402    /// Automatically uses reserve transfer via relay chain
403    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    // Helper methods for encoding XCM types
422
423    #[allow(clippy::result_large_err)]
424    fn encode_multilocation(&self, location: &MultiLocation) -> Result<subxt::dynamic::Value> {
425        // Encode MultiLocation as composite value
426        // Structure: { parents: u8, interior: Junctions }
427
428        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            // X0 (Here) variant
443            return Ok(subxt::dynamic::Value::unnamed_variant("Here", vec![]));
444        }
445
446        // Encode junctions as nested X1, X2, etc.
447        let encoded_junctions: Vec<subxt::dynamic::Value> = junctions
448            .iter()
449            .map(|j| self.encode_junction(j))
450            .collect::<Result<Vec<_>>>()?;
451
452        // Use appropriate variant based on number of junctions
453        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                    // Encode network if present
481                    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        // Wrap in VersionedAssets::V3
553        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        // Wait for finalization
592        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}