1use 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
64const MAX_BLOCK_SEARCH_DEPTH: u32 = 100;
66
67#[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
120pub type Result<T> = std::result::Result<T, Error>;
122
123#[derive(Debug, Clone)]
125pub struct ChainConfig {
126 pub name: String,
128 pub endpoint: String,
130 pub ss58_prefix: u16,
132 pub token_symbol: String,
134 pub token_decimals: u8,
136}
137
138impl ChainConfig {
139 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 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 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 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 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
195pub struct SubstrateAdapter {
197 endpoint: String,
199 client: OnlineClient<PolkadotConfig>,
201 config: ChainConfig,
203 connected: bool,
205 metrics: Metrics,
207 monitor: Arc<OnceCell<Arc<monitor::TransactionMonitor>>>,
209}
210
211impl SubstrateAdapter {
212 pub async fn connect(endpoint: &str) -> Result<Self> {
214 Self::connect_with_config(ChainConfig::custom("Substrate", endpoint, 42)).await
215 }
216
217 pub async fn connect_with_config(config: ChainConfig) -> Result<Self> {
219 info!("Connecting to {} at {}", config.name, config.endpoint);
220
221 let client = OnlineClient::<PolkadotConfig>::from_url(&config.endpoint)
223 .await
224 .map_err(|e| Error::Connection(format!("Failed to connect: {}", e)))?;
225
226 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 pub fn client(&self) -> &OnlineClient<PolkadotConfig> {
242 &self.client
243 }
244
245 pub fn endpoint(&self) -> &str {
247 &self.endpoint
248 }
249
250 pub fn config(&self) -> &ChainConfig {
252 &self.config
253 }
254
255 pub fn is_connected(&self) -> bool {
257 self.connected
258 }
259
260 pub fn metrics(&self) -> MetricsSnapshot {
262 self.metrics.snapshot()
263 }
264
265 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 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 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 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 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 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 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 let mut blocks_to_check = vec![];
348 let start_num = latest_number.saturating_sub(MAX_BLOCK_SEARCH_DEPTH);
349
350 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 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, }
362 }
363 _ => break,
364 }
365 }
366
367 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 let extrinsics = block
378 .extrinsics()
379 .await
380 .map_err(|e| Error::Transaction(format!("Failed to get extrinsics: {}", e)))?;
381
382 for ext_details in extrinsics.iter() {
384 let ext_bytes = ext_details.bytes();
387 let computed_hash = sp_core::blake2_256(ext_bytes);
388
389 if computed_hash == hash_array {
390 let ext_index = ext_details.index();
392
393 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 if event.pallet_name() == "System" {
409 if event.variant_name() == "ExtrinsicSuccess" {
410 success = true;
411 } else if event.variant_name() == "ExtrinsicFailed" {
412 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 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 Ok(TransactionStatus::unknown(tx_hash.to_string()))
436 };
437 }
438 }
439 }
440
441 Ok(TransactionStatus::unknown(tx_hash.to_string()))
443 }
444
445 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); let max_poll_interval = std::time::Duration::from_secs(5); while start.elapsed() < timeout {
466 match self.get_transaction_status(tx_hash).await {
467 Ok(status) => {
468 let is_satisfied = match strategy {
470 ConfirmationStrategy::Immediate => {
471 status.status != TxStatus::Pending && status.status != TxStatus::Unknown
473 }
474 ConfirmationStrategy::Finalized { .. } => {
475 status.status == TxStatus::Finalized
477 || status.status == TxStatus::Confirmed
478 || status.status == TxStatus::Failed
479 }
480 ConfirmationStrategy::BlockConfirmations {
481 confirmations: required,
482 ..
483 } => {
484 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 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 pub fn validate_address(&self, address: &Address) -> bool {
516 match address {
517 Address::Substrate(addr) => {
518 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 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 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 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 let decoded = account_data
562 .to_value()
563 .map_err(|e| Error::Storage(format!("Failed to decode account data: {}", e)))?;
564
565 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 debug!("Account {} not found, returning 0 balance", address);
580 Ok(0)
581 }
582 }
583
584 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 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 pub fn storage(&self) -> StorageClient {
611 StorageClient::new(self.client.clone(), self.metrics.clone())
612 }
613
614 pub fn transaction_executor(&self) -> TransactionExecutor {
616 TransactionExecutor::new(self.client.clone(), self.metrics.clone())
617 }
618
619 pub fn fee_estimator(&self) -> DynamicFeeEstimator {
625 DynamicFeeEstimator::new(self.client.clone())
626 }
627
628 pub fn runtime_version(&self) -> u32 {
630 self.client.runtime_version().spec_version
631 }
632
633 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 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 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 Ok(1_000_000u128)
702 }
703 }
704 }
705
706 async fn get_block(&self, block_number: u64) -> std::result::Result<BlockInfo, SdkError> {
707 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 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 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 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 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 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 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 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), }
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(_) => {} _ => panic!("Expected Substrate address"),
994 }
995
996 match evm_addr {
997 Address::Evm(_) => {} _ => 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); 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; 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 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(_) => {} _ => panic!("Expected Subxt error variant"),
1102 }
1103 }
1104
1105 #[tokio::test]
1107 #[ignore] 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] 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] 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] 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}