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    fn encode_multilocation(&self, location: &MultiLocation) -> Result<subxt::dynamic::Value> {
424        // Encode MultiLocation as composite value
425        // Structure: { parents: u8, interior: Junctions }
426
427        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            // X0 (Here) variant
441            return Ok(subxt::dynamic::Value::unnamed_variant("Here", vec![]));
442        }
443
444        // Encode junctions as nested X1, X2, etc.
445        let encoded_junctions: Vec<subxt::dynamic::Value> = junctions
446            .iter()
447            .map(|j| self.encode_junction(j))
448            .collect::<Result<Vec<_>>>()?;
449
450        // Use appropriate variant based on number of junctions
451        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                    // Encode network if present
478                    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        // Wrap in VersionedAssets::V3
549        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        // Wait for finalization
587        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}