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    #[allow(clippy::derivable_impls)]
259    pub version: XcmVersion,
260    /// Weight limit for execution
261    pub weight_limit: WeightLimit,
262    /// Fee asset to use (defaults to native token)
263    pub fee_asset: Option<XcmAsset>,
264}
265
266/// XCM executor for sending cross-chain messages
267pub struct XcmExecutor {
268    client: OnlineClient<PolkadotConfig>,
269    config: XcmConfig,
270}
271
272impl XcmExecutor {
273    /// Create a new XCM executor
274    pub fn new(client: OnlineClient<PolkadotConfig>) -> Self {
275        Self {
276            client,
277            config: XcmConfig::default(),
278        }
279    }
280
281    /// Create a new XCM executor with custom configuration
282    pub fn with_config(client: OnlineClient<PolkadotConfig>, config: XcmConfig) -> Self {
283        Self { client, config }
284    }
285
286    /// Set the XCM version
287    pub fn with_version(mut self, version: XcmVersion) -> Self {
288        self.config.version = version;
289        self
290    }
291
292    /// Set the weight limit
293    pub fn with_weight_limit(mut self, limit: WeightLimit) -> Self {
294        self.config.weight_limit = limit;
295        self
296    }
297
298    /// Execute a reserve transfer to another chain
299    ///
300    /// # Arguments
301    ///
302    /// * `wallet` - Wallet to sign the transaction
303    /// * `dest` - Destination multi-location (parachain or relay chain)
304    /// * `beneficiary` - Beneficiary account on the destination chain
305    /// * `assets` - Assets to transfer
306    ///
307    /// # Returns
308    ///
309    /// Transaction hash of the XCM transfer extrinsic
310    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        // Build the reserve transfer call using dynamic API
320        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; // Use first asset for fees
324
325        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    /// Execute a teleport transfer to another chain
341    ///
342    /// # Arguments
343    ///
344    /// * `wallet` - Wallet to sign the transaction
345    /// * `dest` - Destination multi-location
346    /// * `beneficiary` - Beneficiary account on the destination chain
347    /// * `assets` - Assets to transfer
348    ///
349    /// # Returns
350    ///
351    /// Transaction hash of the XCM transfer extrinsic
352    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    /// Transfer to relay chain (convenience method)
382    ///
383    /// Automatically uses reserve transfer to parent chain
384    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    /// Transfer to another parachain
402    ///
403    /// Automatically uses reserve transfer via relay chain
404    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    // Helper methods for encoding XCM types
423
424    #[allow(clippy::result_large_err)]
425    fn encode_multilocation(&self, location: &MultiLocation) -> Result<subxt::dynamic::Value> {
426        // Encode MultiLocation as composite value
427        // Structure: { parents: u8, interior: Junctions }
428
429        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            // X0 (Here) variant
444            return Ok(subxt::dynamic::Value::unnamed_variant("Here", vec![]));
445        }
446
447        // Encode junctions as nested X1, X2, etc.
448        let encoded_junctions: Vec<subxt::dynamic::Value> = junctions
449            .iter()
450            .map(|j| self.encode_junction(j))
451            .collect::<Result<Vec<_>>>()?;
452
453        // Use appropriate variant based on number of junctions
454        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                    // Encode network if present
482                    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        // Wrap in VersionedAssets::V3
554        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        // Wait for finalization
593        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}