Skip to main content

apex_sdk_substrate/
lib.rs

1//! Substrate blockchain adapter
2//!
3//! This module provides a comprehensive adapter for interacting with Substrate-based blockchains.
4//! It includes support for:
5//! - Connection management via WebSocket
6//! - Account and wallet management (SR25519, ED25519)
7//! - Transaction execution (extrinsics)
8//! - Storage queries
9//! - Connection pooling
10//! - Caching
11//! - Metrics collection
12
13use apex_sdk_core::{
14    BlockInfo, Broadcaster, ConfirmationStrategy, NonceManager, Provider as CoreProvider,
15    ReceiptWatcher, SdkError,
16};
17use apex_sdk_types::{Address, TransactionStatus, TxStatus};
18use async_trait::async_trait;
19use std::sync::Arc;
20use subxt::{OnlineClient, PolkadotConfig};
21use thiserror::Error;
22use tokio::sync::OnceCell;
23use tracing::{debug, info};
24
25pub mod block;
26pub mod cache;
27pub mod contracts;
28pub mod fee_estimator;
29pub mod metrics;
30pub mod monitor;
31pub mod nonce_manager;
32pub mod pool;
33pub mod signer;
34pub mod storage;
35pub mod transaction;
36pub mod wallet;
37pub mod xcm;
38
39#[cfg(feature = "typed")]
40pub mod metadata;
41
42pub use block::BlockQuery;
43pub use cache::{Cache, CacheConfig};
44pub use contracts::{
45    parse_metadata, ContractCallBuilder, ContractClient, ContractMetadata, GasLimit,
46    StorageDepositLimit,
47};
48pub use fee_estimator::{
49    CongestionLevel, DynamicFeeEstimator, FeeAccuracyMetric, FeeAccuracyStats, FeeEstimate,
50    FeeStrategy, NetworkCongestion, Weight,
51};
52pub use metrics::{Metrics, MetricsSnapshot};
53pub use nonce_manager::SubstrateNonceManager;
54pub use pool::{ConnectionPool, PoolConfig};
55pub use signer::{ApexSigner, Ed25519Signer, Sr25519Signer};
56pub use storage::{AccountInfo, StorageClient, StorageQuery};
57pub use transaction::{BatchCall, BatchMode, FeeConfig, RetryConfig, TransactionExecutor};
58pub use wallet::{KeyPairType, Wallet, WalletManager};
59pub use xcm::{
60    AssetId, Fungibility, Junction, MultiLocation, NetworkId, WeightLimit, XcmAsset, XcmConfig,
61    XcmExecutor, XcmTransferType, XcmVersion,
62};
63
64/// Maximum number of blocks to search when looking up transaction history
65const MAX_BLOCK_SEARCH_DEPTH: u32 = 100;
66
67/// Substrate adapter error
68#[derive(Error, Debug)]
69pub enum Error {
70    #[error("Connection error: {0}")]
71    Connection(String),
72
73    #[error("Transaction error: {0}")]
74    Transaction(String),
75
76    #[error("Metadata error: {0}")]
77    Metadata(String),
78
79    #[error("Storage error: {0}")]
80    Storage(String),
81
82    #[error("Wallet error: {0}")]
83    Wallet(String),
84
85    #[error("Signature error: {0}")]
86    Signature(String),
87
88    #[error("Encoding error: {0}")]
89    Encoding(String),
90
91    #[error("Subxt error: {0}")]
92    Subxt(Box<subxt::Error>),
93
94    #[error("Other error: {0}")]
95    Other(String),
96}
97
98impl From<subxt::Error> for Error {
99    fn from(err: subxt::Error) -> Self {
100        Error::Subxt(Box::new(err))
101    }
102}
103
104impl From<Error> for SdkError {
105    fn from(err: Error) -> Self {
106        match err {
107            Error::Connection(msg) => SdkError::NetworkError(msg),
108            Error::Transaction(msg) => SdkError::TransactionError(msg),
109            Error::Metadata(msg) => SdkError::ConfigError(msg),
110            Error::Storage(msg) => SdkError::ProviderError(msg),
111            Error::Wallet(msg) => SdkError::SignerError(msg),
112            Error::Signature(msg) => SdkError::SignerError(msg),
113            Error::Encoding(msg) => SdkError::TransactionError(msg),
114            Error::Subxt(err) => SdkError::ProviderError(err.to_string()),
115            Error::Other(msg) => SdkError::ProviderError(msg),
116        }
117    }
118}
119
120/// Type alias for Result with our Error type
121pub type Result<T> = std::result::Result<T, Error>;
122
123/// Chain configuration for different Substrate chains
124#[derive(Debug, Clone)]
125pub struct ChainConfig {
126    /// Chain name
127    pub name: String,
128    /// WebSocket endpoint
129    pub endpoint: String,
130    /// SS58 address prefix
131    pub ss58_prefix: u16,
132    /// Token symbol
133    pub token_symbol: String,
134    /// Token decimals
135    pub token_decimals: u8,
136}
137
138impl ChainConfig {
139    /// Create configuration for Polkadot
140    pub fn polkadot() -> Self {
141        Self {
142            name: "Polkadot".to_string(),
143            endpoint: "wss://rpc.polkadot.io".to_string(),
144            ss58_prefix: 0,
145            token_symbol: "DOT".to_string(),
146            token_decimals: 10,
147        }
148    }
149
150    /// Create configuration for Kusama
151    pub fn kusama() -> Self {
152        Self {
153            name: "Kusama".to_string(),
154            endpoint: "wss://kusama-rpc.polkadot.io".to_string(),
155            ss58_prefix: 2,
156            token_symbol: "KSM".to_string(),
157            token_decimals: 12,
158        }
159    }
160
161    /// Create configuration for Westend (testnet)
162    pub fn westend() -> Self {
163        Self {
164            name: "Westend".to_string(),
165            endpoint: "wss://westend-rpc.polkadot.io".to_string(),
166            ss58_prefix: 42,
167            token_symbol: "WND".to_string(),
168            token_decimals: 12,
169        }
170    }
171
172    /// Create configuration for Paseo (testnet)
173    pub fn paseo() -> Self {
174        Self {
175            name: "Paseo".to_string(),
176            endpoint: "wss://paseo.rpc.amforc.com".to_string(),
177            ss58_prefix: 42,
178            token_symbol: "PAS".to_string(),
179            token_decimals: 10,
180        }
181    }
182
183    /// Create custom configuration
184    pub fn custom(name: impl Into<String>, endpoint: impl Into<String>, ss58_prefix: u16) -> Self {
185        Self {
186            name: name.into(),
187            endpoint: endpoint.into(),
188            ss58_prefix,
189            token_symbol: "UNIT".to_string(),
190            token_decimals: 12,
191        }
192    }
193}
194
195/// Substrate blockchain adapter
196pub struct SubstrateAdapter {
197    /// WebSocket endpoint
198    endpoint: String,
199    /// Subxt client
200    client: OnlineClient<PolkadotConfig>,
201    /// Chain configuration
202    config: ChainConfig,
203    /// Connection status
204    connected: bool,
205    /// Metrics collector
206    metrics: Metrics,
207    /// Transaction monitor for subscription-based monitoring (lazy-initialized)
208    monitor: Arc<OnceCell<Arc<monitor::TransactionMonitor>>>,
209}
210
211impl SubstrateAdapter {
212    /// Connect to a Substrate node using default Polkadot configuration
213    pub async fn connect(endpoint: &str) -> Result<Self> {
214        Self::connect_with_config(ChainConfig::custom("Substrate", endpoint, 42)).await
215    }
216
217    /// Connect to a Substrate node with specific chain configuration
218    pub async fn connect_with_config(config: ChainConfig) -> Result<Self> {
219        info!("Connecting to {} at {}", config.name, config.endpoint);
220
221        // Create subxt client
222        let client = OnlineClient::<PolkadotConfig>::from_url(&config.endpoint)
223            .await
224            .map_err(|e| Error::Connection(format!("Failed to connect: {}", e)))?;
225
226        // Verify connection by fetching metadata
227        let _metadata = client.metadata();
228        debug!("Connected to {}", config.name);
229
230        Ok(Self {
231            endpoint: config.endpoint.clone(),
232            client,
233            config,
234            connected: true,
235            metrics: Metrics::new(),
236            monitor: Arc::new(OnceCell::new()),
237        })
238    }
239
240    /// Get reference to the subxt client
241    pub fn client(&self) -> &OnlineClient<PolkadotConfig> {
242        &self.client
243    }
244
245    /// Get the endpoint URL
246    pub fn endpoint(&self) -> &str {
247        &self.endpoint
248    }
249
250    /// Get the chain configuration
251    pub fn config(&self) -> &ChainConfig {
252        &self.config
253    }
254
255    /// Check if connected
256    pub fn is_connected(&self) -> bool {
257        self.connected
258    }
259
260    /// Get metrics snapshot
261    pub fn metrics(&self) -> MetricsSnapshot {
262        self.metrics.snapshot()
263    }
264
265    /// Get or initialize the transaction monitor
266    async fn get_monitor(&self) -> Result<Arc<monitor::TransactionMonitor>> {
267        self.monitor
268            .get_or_try_init(|| async {
269                monitor::TransactionMonitor::new(
270                    self.client.clone(),
271                    Arc::new(self.metrics.clone()),
272                )
273                .await
274                .map(Arc::new)
275            })
276            .await
277            .cloned()
278    }
279
280    /// Get block by hash
281    ///
282    /// This is more efficient than get_block if you have the block hash.
283    pub async fn get_block_by_hash(&self, block_hash: &str) -> Result<BlockInfo> {
284        let block_query = crate::block::BlockQuery::new(self.client.clone());
285        block_query.get_block_by_hash(block_hash).await
286    }
287
288    /// Get detailed block information including extrinsics and events
289    ///
290    /// This provides comprehensive block data for advanced analysis.
291    pub async fn get_block_detailed(
292        &self,
293        block_number: u64,
294    ) -> Result<apex_sdk_core::DetailedBlockInfo> {
295        let block_query = crate::block::BlockQuery::new(self.client.clone());
296        block_query.get_detailed_block(block_number).await
297    }
298
299    /// Get events from a specific block
300    ///
301    /// Returns all events that occurred in the specified block.
302    pub async fn get_block_events(
303        &self,
304        block_number: u64,
305    ) -> Result<Vec<apex_sdk_core::BlockEvent>> {
306        let detailed = self.get_block_detailed(block_number).await?;
307        Ok(detailed.events)
308    }
309
310    /// Get transaction status by extrinsic hash
311    pub async fn get_transaction_status(&self, tx_hash: &str) -> Result<TransactionStatus> {
312        if !self.connected {
313            return Err(Error::Connection("Not connected".to_string()));
314        }
315
316        debug!("Getting transaction status for: {}", tx_hash);
317        self.metrics.record_rpc_call("get_transaction_status");
318
319        // Parse the transaction hash
320        let hash_bytes = hex::decode(tx_hash.trim_start_matches("0x"))
321            .map_err(|e| Error::Transaction(format!("Invalid transaction hash: {}", e)))?;
322
323        if hash_bytes.len() != 32 {
324            return Err(Error::Transaction(
325                "Transaction hash must be 32 bytes".to_string(),
326            ));
327        }
328
329        let mut hash_array = [0u8; 32];
330        hash_array.copy_from_slice(&hash_bytes);
331
332        // Try to subscribe to finalized blocks and check recent history
333        // Note: This is a simplified implementation that checks recent finalized blocks
334        // For production, consider maintaining a transaction pool and using event subscriptions
335
336        // Get the latest finalized block
337        let latest_block = self
338            .client
339            .blocks()
340            .at_latest()
341            .await
342            .map_err(|e| Error::Connection(format!("Failed to get latest block: {}", e)))?;
343
344        let latest_number = latest_block.number();
345
346        // Search backwards through recent blocks
347        let mut blocks_to_check = vec![];
348        let start_num = latest_number.saturating_sub(MAX_BLOCK_SEARCH_DEPTH);
349
350        // Subscribe to finalized blocks and iterate backwards
351        let mut current_block = latest_block;
352        for _ in 0..MAX_BLOCK_SEARCH_DEPTH {
353            blocks_to_check.push((current_block.number(), current_block.hash()));
354
355            // Try to get parent block
356            match current_block.header().parent_hash {
357                parent_hash if current_block.number() > start_num => {
358                    match self.client.blocks().at(parent_hash).await {
359                        Ok(parent) => current_block = parent,
360                        Err(_) => break, // Can't go further back
361                    }
362                }
363                _ => break,
364            }
365        }
366
367        // Now check all collected blocks for the transaction
368        for (block_num, block_hash) in blocks_to_check {
369            let block = self
370                .client
371                .blocks()
372                .at(block_hash)
373                .await
374                .map_err(|e| Error::Connection(format!("Failed to get block: {}", e)))?;
375
376            // Get extrinsics from the block
377            let extrinsics = block
378                .extrinsics()
379                .await
380                .map_err(|e| Error::Transaction(format!("Failed to get extrinsics: {}", e)))?;
381
382            // Compute hash for each extrinsic and compare
383            for ext_details in extrinsics.iter() {
384                // ext_details is already an ExtrinsicDetails, no need for map_err
385                // Compute the hash from the extrinsic bytes
386                let ext_bytes = ext_details.bytes();
387                let computed_hash = sp_core::blake2_256(ext_bytes);
388
389                if computed_hash == hash_array {
390                    // Found the transaction! Get the extrinsic index
391                    let ext_index = ext_details.index();
392
393                    // Check events for this extrinsic
394                    let events = ext_details
395                        .events()
396                        .await
397                        .map_err(|e| Error::Transaction(format!("Failed to get events: {}", e)))?;
398
399                    let mut success = false;
400                    let mut error_msg = None;
401
402                    for event in events.iter() {
403                        let event = event.map_err(|e| {
404                            Error::Transaction(format!("Failed to decode event: {}", e))
405                        })?;
406
407                        // Check for ExtrinsicSuccess or ExtrinsicFailed
408                        if event.pallet_name() == "System" {
409                            if event.variant_name() == "ExtrinsicSuccess" {
410                                success = true;
411                            } else if event.variant_name() == "ExtrinsicFailed" {
412                                // Try to extract error details from event
413                                error_msg = Some(format!("Extrinsic {} failed", ext_index));
414                            }
415                        }
416                    }
417
418                    let confirmations = latest_number - block_num;
419
420                    return if success {
421                        // For substrate, we consider a transaction confirmed once it's included in a block
422                        // The confirmation threshold is mainly for documentation purposes
423                        Ok(TransactionStatus::confirmed(
424                            tx_hash.to_string(),
425                            block_num as u64,
426                            block_hash.to_string(),
427                            None,
428                            None,
429                            Some(confirmations),
430                        ))
431                    } else if let Some(error) = error_msg {
432                        Ok(TransactionStatus::failed(tx_hash.to_string(), error))
433                    } else {
434                        // Transaction found but status unclear
435                        Ok(TransactionStatus::unknown(tx_hash.to_string()))
436                    };
437                }
438            }
439        }
440
441        // Transaction not found in recent blocks
442        Ok(TransactionStatus::unknown(tx_hash.to_string()))
443    }
444
445    /// Fallback polling implementation with exponential backoff
446    async fn wait_for_receipt_polling(
447        &self,
448        tx_hash: &str,
449        strategy: &ConfirmationStrategy,
450    ) -> std::result::Result<TransactionStatus, SdkError> {
451        let start = std::time::Instant::now();
452        let timeout = match strategy {
453            ConfirmationStrategy::BlockConfirmations { timeout_secs, .. } => {
454                std::time::Duration::from_secs(*timeout_secs)
455            }
456            ConfirmationStrategy::Finalized { timeout_secs } => {
457                std::time::Duration::from_secs(*timeout_secs)
458            }
459            ConfirmationStrategy::Immediate => std::time::Duration::from_secs(30),
460        };
461
462        let mut poll_interval = std::time::Duration::from_millis(500); // Start at 500ms
463        let max_poll_interval = std::time::Duration::from_secs(5); // Max 5 seconds
464
465        while start.elapsed() < timeout {
466            match self.get_transaction_status(tx_hash).await {
467                Ok(status) => {
468                    // Check if the strategy conditions are met
469                    let is_satisfied = match strategy {
470                        ConfirmationStrategy::Immediate => {
471                            // Any status other than pending/unknown is sufficient
472                            status.status != TxStatus::Pending && status.status != TxStatus::Unknown
473                        }
474                        ConfirmationStrategy::Finalized { .. } => {
475                            // Wait for finalized status
476                            status.status == TxStatus::Finalized
477                                || status.status == TxStatus::Confirmed
478                                || status.status == TxStatus::Failed
479                        }
480                        ConfirmationStrategy::BlockConfirmations {
481                            confirmations: required,
482                            ..
483                        } => {
484                            // Check if we have enough confirmations
485                            if let Some(confirmations) = status.confirmations {
486                                confirmations >= *required
487                            } else {
488                                status.status == TxStatus::Failed
489                            }
490                        }
491                    };
492
493                    if is_satisfied {
494                        debug!("Transaction {} satisfied strategy via polling", tx_hash);
495                        return Ok(status);
496                    }
497                }
498                Err(e) => {
499                    debug!("Error checking transaction status: {}", e);
500                }
501            }
502
503            // Exponential backoff: double the interval up to the max
504            tokio::time::sleep(poll_interval).await;
505            poll_interval = std::cmp::min(poll_interval * 2, max_poll_interval);
506        }
507
508        Err(SdkError::NetworkError(format!(
509            "Timeout waiting for transaction {} after {:?}",
510            tx_hash, timeout
511        )))
512    }
513
514    /// Validate a Substrate address (SS58 format)
515    pub fn validate_address(&self, address: &Address) -> bool {
516        match address {
517            Address::Substrate(addr) => {
518                // Use sp_core to validate SS58 address
519                use sp_core::crypto::Ss58Codec;
520                sp_core::sr25519::Public::from_ss58check(addr).is_ok()
521                    || sp_core::ed25519::Public::from_ss58check(addr).is_ok()
522            }
523            _ => false,
524        }
525    }
526
527    /// Get account balance using dynamic storage queries
528    pub async fn get_balance(&self, address: &str) -> Result<u128> {
529        if !self.connected {
530            return Err(Error::Connection("Not connected".to_string()));
531        }
532
533        debug!("Getting balance for address: {}", address);
534        self.metrics.record_rpc_call("get_balance");
535
536        // Parse SS58 address to get AccountId32
537        use sp_core::crypto::{AccountId32, Ss58Codec};
538        let account_id = AccountId32::from_ss58check(address)
539            .map_err(|e| Error::Storage(format!("Invalid SS58 address: {}", e)))?;
540
541        // Query account info from System pallet using dynamic API
542        let account_bytes: &[u8] = account_id.as_ref();
543        let storage_query = subxt::dynamic::storage(
544            "System",
545            "Account",
546            vec![subxt::dynamic::Value::from_bytes(account_bytes)],
547        );
548
549        let result = self
550            .client
551            .storage()
552            .at_latest()
553            .await
554            .map_err(|e| Error::Storage(format!("Failed to get latest block: {}", e)))?
555            .fetch(&storage_query)
556            .await
557            .map_err(|e| Error::Storage(format!("Failed to query storage: {}", e)))?;
558
559        if let Some(account_data) = result {
560            // Decode the storage value
561            let decoded = account_data
562                .to_value()
563                .map_err(|e| Error::Storage(format!("Failed to decode account data: {}", e)))?;
564
565            // Extract the free balance from the account data
566            // Account structure: { nonce, consumers, providers, sufficients, data: { free, reserved, ... } }
567            use subxt::dynamic::At as _;
568
569            let free_balance = decoded
570                .at("data")
571                .and_then(|data| data.at("free"))
572                .and_then(|free| free.as_u128())
573                .unwrap_or(0);
574
575            debug!("Balance for {}: {}", address, free_balance);
576            Ok(free_balance)
577        } else {
578            // Account doesn't exist, return 0
579            debug!("Account {} not found, returning 0 balance", address);
580            Ok(0)
581        }
582    }
583
584    /// Get formatted balance (with decimals)
585    pub async fn get_balance_formatted(&self, address: &str) -> Result<String> {
586        let balance = self.get_balance(address).await?;
587        let decimals = self.config.token_decimals as u32;
588        // Prevent overflow: 10u128.pow(decimals) will panic if decimals > 38
589        let divisor = if decimals <= 38 {
590            10u128.pow(decimals)
591        } else {
592            return Err(Error::Storage(format!(
593                "Token decimals too large: {}",
594                decimals
595            )));
596        };
597        let whole = balance / divisor;
598        let fraction = balance % divisor;
599
600        Ok(format!(
601            "{}.{:0width$} {}",
602            whole,
603            fraction,
604            self.config.token_symbol,
605            width = decimals as usize
606        ))
607    }
608
609    /// Create a storage client for querying chain storage
610    pub fn storage(&self) -> StorageClient {
611        StorageClient::new(self.client.clone(), self.metrics.clone())
612    }
613
614    /// Create a transaction executor
615    pub fn transaction_executor(&self) -> TransactionExecutor {
616        TransactionExecutor::new(self.client.clone(), self.metrics.clone())
617    }
618
619    /// This provides advanced fee estimation capabilities including:
620    /// - Weight-based dynamic calculations
621    /// - Network congestion monitoring
622    /// - Multiple fee strategies (Fast, Normal, Slow)
623    /// - Fee estimation accuracy metrics
624    pub fn fee_estimator(&self) -> DynamicFeeEstimator {
625        DynamicFeeEstimator::new(self.client.clone())
626    }
627
628    /// Get runtime version
629    pub fn runtime_version(&self) -> u32 {
630        self.client.runtime_version().spec_version
631    }
632
633    /// Get chain name from metadata
634    pub fn chain_name(&self) -> &str {
635        &self.config.name
636    }
637}
638
639#[async_trait]
640impl apex_sdk_core::ChainAdapter for SubstrateAdapter {
641    async fn get_transaction_status(
642        &self,
643        tx_hash: &str,
644    ) -> std::result::Result<TransactionStatus, String> {
645        self.get_transaction_status(tx_hash)
646            .await
647            .map_err(|e| e.to_string())
648    }
649
650    fn validate_address(&self, address: &Address) -> bool {
651        self.validate_address(address)
652    }
653
654    fn chain_name(&self) -> &str {
655        self.chain_name()
656    }
657}
658
659#[async_trait]
660impl CoreProvider for SubstrateAdapter {
661    async fn get_block_number(&self) -> std::result::Result<u64, SdkError> {
662        let block = self
663            .client
664            .blocks()
665            .at_latest()
666            .await
667            .map_err(Error::from)?;
668        Ok(block.number() as u64)
669    }
670
671    async fn get_balance(&self, address: &Address) -> std::result::Result<u128, SdkError> {
672        match address {
673            Address::Substrate(addr) => self.get_balance(addr).await.map_err(Into::into),
674            _ => Err(SdkError::ConfigError(
675                "Invalid address type for Substrate adapter".to_string(),
676            )),
677        }
678    }
679
680    async fn get_transaction_count(&self, address: &Address) -> std::result::Result<u64, SdkError> {
681        match address {
682            Address::Substrate(addr) => {
683                // Use StorageClient to properly query the nonce
684                let storage_client = StorageClient::new(self.client.clone(), self.metrics.clone());
685
686                storage_client.get_nonce(addr).await.map_err(SdkError::from)
687            }
688            _ => Err(SdkError::ConfigError(
689                "Invalid address type for Substrate adapter".to_string(),
690            )),
691        }
692    }
693
694    async fn estimate_fee(&self, tx: &[u8]) -> std::result::Result<u128, SdkError> {
695        // Use the working transaction executor fee estimation
696        match self.transaction_executor().estimate_fee_for_bytes(tx).await {
697            Ok(fee) => Ok(fee),
698            Err(e) => {
699                tracing::warn!("Substrate fee estimation failed: {}", e);
700                // Fallback to a reasonable default (1 million Planck)
701                Ok(1_000_000u128)
702            }
703        }
704    }
705
706    async fn get_block(&self, block_number: u64) -> std::result::Result<BlockInfo, SdkError> {
707        // Use BlockQuery to fetch real blockchain data
708        let block_query = crate::block::BlockQuery::new(self.client.clone());
709
710        block_query
711            .get_block_by_number(block_number)
712            .await
713            .map_err(|e| SdkError::ProviderError(format!("Failed to get block: {}", e)))
714    }
715
716    async fn health_check(&self) -> std::result::Result<(), SdkError> {
717        // Check if we can get the latest block
718        match self.client.blocks().at_latest().await {
719            Ok(_) => Ok(()),
720            Err(e) => Err(SdkError::ProviderError(e.to_string())),
721        }
722    }
723}
724
725#[async_trait]
726impl NonceManager for SubstrateAdapter {
727    async fn get_next_nonce(&self, address: &Address) -> std::result::Result<u64, SdkError> {
728        // For Substrate nonce management, we query the account nonce directly from storage
729        // This gives us the next nonce to use for transactions
730        self.get_transaction_count(address).await
731    }
732}
733
734#[async_trait]
735impl Broadcaster for SubstrateAdapter {
736    async fn broadcast(&self, signed_tx: &[u8]) -> std::result::Result<String, SdkError> {
737        if !self.connected {
738            return Err(SdkError::NetworkError("Not connected to chain".to_string()));
739        }
740
741        if signed_tx.is_empty() {
742            return Err(SdkError::TransactionError(
743                "Cannot broadcast empty transaction".to_string(),
744            ));
745        }
746
747        if signed_tx.len() < 4 {
748            return Err(SdkError::TransactionError(
749                "Transaction too short to be valid extrinsic".to_string(),
750            ));
751        }
752
753        self.validate_extrinsic_format(signed_tx)?;
754
755        self.metrics.record_transaction_attempt();
756        debug!("Broadcasting extrinsic ({} bytes)", signed_tx.len());
757
758        let tx_hash = self
759            .submit_and_watch_extrinsic(signed_tx)
760            .await
761            .map_err(|e| {
762                self.metrics.record_transaction_failure();
763                SdkError::TransactionError(format!("Broadcast failed: {}", e))
764            })?;
765
766        self.metrics.record_transaction_success();
767        info!("Extrinsic broadcast successful: {}", tx_hash);
768
769        Ok(tx_hash)
770    }
771}
772
773impl SubstrateAdapter {
774    fn validate_extrinsic_format(
775        &self,
776        extrinsic_bytes: &[u8],
777    ) -> std::result::Result<(), SdkError> {
778        use parity_scale_codec::Decode;
779
780        let first_byte = extrinsic_bytes[0];
781
782        let has_signature = (first_byte & 0b1000_0000) != 0;
783        if !has_signature {
784            return Err(SdkError::TransactionError(
785                "Extrinsic must be signed for broadcasting".to_string(),
786            ));
787        }
788
789        let version = first_byte & 0b0111_1111;
790        if version != 4 {
791            return Err(SdkError::TransactionError(format!(
792                "Unsupported extrinsic version: {}. Expected version 4",
793                version
794            )));
795        }
796
797        let length_result = parity_scale_codec::Compact::<u32>::decode(&mut &extrinsic_bytes[1..]);
798        if length_result.is_err() {
799            return Err(SdkError::TransactionError(
800                "Invalid extrinsic length encoding".to_string(),
801            ));
802        }
803
804        Ok(())
805    }
806
807    async fn submit_and_watch_extrinsic(&self, extrinsic_bytes: &[u8]) -> Result<String> {
808        use subxt::backend::{legacy::LegacyRpcMethods, rpc::RpcClient};
809
810        let rpc_client = RpcClient::from_url(&self.endpoint)
811            .await
812            .map_err(|e| Error::Connection(format!("Failed to create RPC client: {}", e)))?;
813
814        let legacy_rpc = LegacyRpcMethods::<PolkadotConfig>::new(rpc_client);
815
816        let tx_hash = legacy_rpc
817            .author_submit_extrinsic(extrinsic_bytes)
818            .await
819            .map_err(|e| Error::Transaction(format!("Failed to submit extrinsic: {}", e)))?;
820
821        let hash_string = format!("0x{}", hex::encode(tx_hash.0));
822
823        debug!("Extrinsic submitted with hash: {}", hash_string);
824
825        Ok(hash_string)
826    }
827}
828
829#[async_trait]
830impl ReceiptWatcher for SubstrateAdapter {
831    async fn wait_for_receipt(
832        &self,
833        tx_hash: &str,
834    ) -> std::result::Result<TransactionStatus, SdkError> {
835        // Use default finalized strategy
836        let strategy = ConfirmationStrategy::Finalized { timeout_secs: 60 };
837        self.wait_for_receipt_with_strategy(tx_hash, &strategy)
838            .await
839    }
840
841    async fn wait_for_receipt_with_strategy(
842        &self,
843        tx_hash: &str,
844        strategy: &ConfirmationStrategy,
845    ) -> std::result::Result<TransactionStatus, SdkError> {
846        debug!("Waiting for receipt with strategy: {:?}", strategy);
847
848        // Try subscription-based monitoring first
849        match self.get_monitor().await {
850            Ok(monitor) => {
851                debug!("Using subscription-based monitoring for {}", tx_hash);
852
853                let rx = monitor
854                    .watch_transaction(tx_hash.to_string(), strategy.clone())
855                    .await;
856
857                // Wait for the result with timeout
858                let timeout = match strategy {
859                    ConfirmationStrategy::BlockConfirmations { timeout_secs, .. } => {
860                        std::time::Duration::from_secs(*timeout_secs)
861                    }
862                    ConfirmationStrategy::Finalized { timeout_secs } => {
863                        std::time::Duration::from_secs(*timeout_secs)
864                    }
865                    ConfirmationStrategy::Immediate => {
866                        // For immediate, still wait a bit for the transaction to be included
867                        std::time::Duration::from_secs(30)
868                    }
869                };
870
871                match tokio::time::timeout(timeout, rx).await {
872                    Ok(Ok(status)) => {
873                        debug!("Subscription monitoring completed for {}", tx_hash);
874                        return Ok(status);
875                    }
876                    Ok(Err(_)) => {
877                        debug!("Subscription channel closed, falling back to polling");
878                    }
879                    Err(_) => {
880                        debug!("Subscription monitoring timed out, falling back to polling");
881                    }
882                }
883            }
884            Err(e) => {
885                debug!(
886                    "Failed to initialize monitor: {}, falling back to polling",
887                    e
888                );
889            }
890        }
891
892        // Fallback to polling with exponential backoff
893        info!("Using polling fallback for {}", tx_hash);
894        self.wait_for_receipt_polling(tx_hash, strategy).await
895    }
896
897    async fn get_receipt_status(
898        &self,
899        tx_hash: &str,
900    ) -> std::result::Result<Option<TransactionStatus>, SdkError> {
901        match self.get_transaction_status(tx_hash).await {
902            Ok(status) => Ok(Some(status)),
903            Err(_) => Ok(None), // If error, assume transaction not found
904        }
905    }
906}
907
908#[cfg(test)]
909mod tests {
910    use super::*;
911    use apex_sdk_types::Address;
912
913    #[test]
914    fn test_chain_config_polkadot() {
915        let polkadot = ChainConfig::polkadot();
916        assert_eq!(polkadot.name, "Polkadot");
917        assert_eq!(polkadot.ss58_prefix, 0);
918        assert_eq!(polkadot.token_symbol, "DOT");
919        assert_eq!(polkadot.token_decimals, 10);
920        assert!(polkadot.endpoint.starts_with("wss://"));
921    }
922
923    #[test]
924    fn test_chain_config_kusama() {
925        let kusama = ChainConfig::kusama();
926        assert_eq!(kusama.name, "Kusama");
927        assert_eq!(kusama.ss58_prefix, 2);
928        assert_eq!(kusama.token_symbol, "KSM");
929        assert_eq!(kusama.token_decimals, 12);
930        assert!(kusama.endpoint.starts_with("wss://"));
931    }
932
933    #[test]
934    fn test_chain_config_westend() {
935        let westend = ChainConfig::westend();
936        assert_eq!(westend.name, "Westend");
937        assert_eq!(westend.ss58_prefix, 42);
938        assert_eq!(westend.token_symbol, "WND");
939        assert_eq!(westend.token_decimals, 12);
940        assert!(westend.endpoint.starts_with("wss://"));
941    }
942
943    #[test]
944    fn test_chain_config_paseo() {
945        let paseo = ChainConfig::paseo();
946        assert_eq!(paseo.name, "Paseo");
947        assert_eq!(paseo.ss58_prefix, 42);
948        assert_eq!(paseo.token_symbol, "PAS");
949        assert_eq!(paseo.token_decimals, 10);
950        assert_eq!(paseo.endpoint, "wss://paseo.rpc.amforc.com");
951    }
952
953    #[test]
954    fn test_chain_config_custom() {
955        let custom = ChainConfig::custom("TestChain", "wss://test.endpoint", 999);
956        assert_eq!(custom.name, "TestChain");
957        assert_eq!(custom.endpoint, "wss://test.endpoint");
958        assert_eq!(custom.ss58_prefix, 999);
959        assert_eq!(custom.token_symbol, "UNIT");
960        assert_eq!(custom.token_decimals, 12);
961    }
962
963    #[test]
964    fn test_address_validation_valid_substrate() {
965        let polkadot_addr = Address::substrate("15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5");
966        let kusama_addr = Address::substrate("HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F");
967        let westend_addr = Address::substrate("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY");
968
969        match polkadot_addr {
970            Address::Substrate(addr) => assert!(!addr.is_empty()),
971            _ => panic!("Expected Substrate address"),
972        }
973
974        match kusama_addr {
975            Address::Substrate(addr) => assert!(!addr.is_empty()),
976            _ => panic!("Expected Substrate address"),
977        }
978
979        match westend_addr {
980            Address::Substrate(addr) => assert!(!addr.is_empty()),
981            _ => panic!("Expected Substrate address"),
982        }
983    }
984
985    #[test]
986    fn test_address_validation_invalid() {
987        let invalid_addr = Address::substrate("invalid_address");
988        let _short_addr = Address::substrate("123");
989        let evm_addr = Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7");
990
991        match invalid_addr {
992            Address::Substrate(_) => {} // Expected Substrate address
993            _ => panic!("Expected Substrate address"),
994        }
995
996        match evm_addr {
997            Address::Evm(_) => {} // Expected EVM address
998            _ => panic!("Expected EVM address"),
999        }
1000    }
1001
1002    #[test]
1003    fn test_chain_adapter_trait_implementation() {
1004        let config = ChainConfig::custom("MockChain", "wss://mock.endpoint", 42);
1005        assert_eq!(config.name, "MockChain");
1006    }
1007
1008    #[test]
1009    fn test_get_balance_validation() {
1010        let valid_substrate_addr = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY";
1011        assert!(valid_substrate_addr.len() > 40); // SS58 addresses are typically longer than this
1012        assert!(valid_substrate_addr.chars().all(|c| c.is_alphanumeric()));
1013    }
1014
1015    #[test]
1016    fn test_format_balance_calculations() {
1017        let decimals = 12u8;
1018        let amount = 1_000_000_000_000u128; // 1 token with 12 decimals
1019
1020        let divisor = 10u128.pow(decimals as u32);
1021        let whole = amount / divisor;
1022        let fraction = amount % divisor;
1023
1024        assert_eq!(whole, 1);
1025        assert_eq!(fraction, 0);
1026    }
1027
1028    #[test]
1029    fn test_chain_specific_prefixes() {
1030        assert_eq!(ChainConfig::polkadot().ss58_prefix, 0);
1031        assert_eq!(ChainConfig::kusama().ss58_prefix, 2);
1032        assert_eq!(ChainConfig::westend().ss58_prefix, 42);
1033        assert_eq!(ChainConfig::paseo().ss58_prefix, 42);
1034    }
1035
1036    #[test]
1037    fn test_token_symbols() {
1038        assert_eq!(ChainConfig::polkadot().token_symbol, "DOT");
1039        assert_eq!(ChainConfig::kusama().token_symbol, "KSM");
1040        assert_eq!(ChainConfig::westend().token_symbol, "WND");
1041        assert_eq!(ChainConfig::paseo().token_symbol, "PAS");
1042    }
1043
1044    #[test]
1045    fn test_constants() {
1046        assert_eq!(MAX_BLOCK_SEARCH_DEPTH, 100);
1047    }
1048
1049    #[test]
1050    fn test_error_types() {
1051        let connection_err = Error::Connection("Test connection error".to_string());
1052        assert_eq!(
1053            connection_err.to_string(),
1054            "Connection error: Test connection error"
1055        );
1056
1057        let transaction_err = Error::Transaction("Test transaction error".to_string());
1058        assert_eq!(
1059            transaction_err.to_string(),
1060            "Transaction error: Test transaction error"
1061        );
1062
1063        let metadata_err = Error::Metadata("Test metadata error".to_string());
1064        assert_eq!(
1065            metadata_err.to_string(),
1066            "Metadata error: Test metadata error"
1067        );
1068
1069        let storage_err = Error::Storage("Test storage error".to_string());
1070        assert_eq!(storage_err.to_string(), "Storage error: Test storage error");
1071
1072        let wallet_err = Error::Wallet("Test wallet error".to_string());
1073        assert_eq!(wallet_err.to_string(), "Wallet error: Test wallet error");
1074
1075        let signature_err = Error::Signature("Test signature error".to_string());
1076        assert_eq!(
1077            signature_err.to_string(),
1078            "Signature error: Test signature error"
1079        );
1080
1081        let encoding_err = Error::Encoding("Test encoding error".to_string());
1082        assert_eq!(
1083            encoding_err.to_string(),
1084            "Encoding error: Test encoding error"
1085        );
1086
1087        let other_err = Error::Other("Test other error".to_string());
1088        assert_eq!(other_err.to_string(), "Other error: Test other error");
1089    }
1090
1091    #[test]
1092    fn test_from_subxt_error() {
1093        use subxt::Error as SubxtError;
1094
1095        // Create a simple error that we can convert
1096        let subxt_err = SubxtError::Other("Test RPC error".to_string());
1097        let our_error: Error = subxt_err.into();
1098
1099        match our_error {
1100            Error::Subxt(_) => {} // Expected
1101            _ => panic!("Expected Subxt error variant"),
1102        }
1103    }
1104
1105    // Integration tests (require network connection)
1106    #[tokio::test]
1107    #[ignore] // Requires network connection
1108    async fn test_substrate_adapter_connect_integration() {
1109        let adapter = SubstrateAdapter::connect("wss://westend-rpc.polkadot.io").await;
1110        assert!(adapter.is_ok());
1111
1112        let adapter = adapter.unwrap();
1113        assert!(adapter.is_connected());
1114        assert_eq!(adapter.chain_name(), "Substrate");
1115    }
1116
1117    #[tokio::test]
1118    #[ignore] // Requires network connection
1119    async fn test_polkadot_connection_integration() {
1120        let adapter = SubstrateAdapter::connect_with_config(ChainConfig::polkadot()).await;
1121        assert!(adapter.is_ok());
1122
1123        let adapter = adapter.unwrap();
1124        assert_eq!(adapter.chain_name(), "Polkadot");
1125    }
1126
1127    #[tokio::test]
1128    #[ignore] // Requires network connection
1129    async fn test_get_balance_integration() {
1130        let adapter = SubstrateAdapter::connect("wss://westend-rpc.polkadot.io")
1131            .await
1132            .unwrap();
1133
1134        let result = adapter
1135            .get_balance("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY")
1136            .await;
1137        assert!(result.is_ok());
1138    }
1139
1140    #[tokio::test]
1141    #[ignore] // Requires network connection
1142    async fn test_invalid_endpoint_connection() {
1143        let result = SubstrateAdapter::connect("wss://invalid.endpoint.that.does.not.exist").await;
1144        assert!(result.is_err());
1145    }
1146}