1use apex_sdk_types::{Address, TransactionStatus};
14use async_trait::async_trait;
15use subxt::{OnlineClient, PolkadotConfig};
16use thiserror::Error;
17use tracing::{debug, info};
18
19pub mod cache;
20pub mod contracts;
21pub mod metrics;
22pub mod pool;
23pub mod signer;
24pub mod storage;
25pub mod transaction;
26pub mod wallet;
27pub mod xcm;
28
29#[cfg(feature = "typed")]
30pub mod metadata;
31
32pub use cache::{Cache, CacheConfig};
33pub use contracts::{
34 parse_metadata, ContractCallBuilder, ContractClient, ContractMetadata, GasLimit,
35 StorageDepositLimit,
36};
37pub use metrics::{Metrics, MetricsSnapshot};
38pub use pool::{ConnectionPool, PoolConfig};
39pub use signer::{ApexSigner, Ed25519Signer, Sr25519Signer};
40pub use storage::{StorageClient, StorageQuery};
41pub use transaction::{
42 BatchCall, BatchMode, ExtrinsicBuilder, FeeConfig, RetryConfig, TransactionExecutor,
43};
44pub use wallet::{KeyPairType, Wallet, WalletManager};
45pub use xcm::{
46 AssetId, Fungibility, Junction, MultiLocation, NetworkId, WeightLimit, XcmAsset, XcmConfig,
47 XcmExecutor, XcmTransferType, XcmVersion,
48};
49
50const FINALIZATION_THRESHOLD: u32 = 10;
53
54const MAX_BLOCK_SEARCH_DEPTH: u32 = 100;
56
57#[derive(Error, Debug)]
59pub enum Error {
60 #[error("Connection error: {0}")]
61 Connection(String),
62
63 #[error("Transaction error: {0}")]
64 Transaction(String),
65
66 #[error("Metadata error: {0}")]
67 Metadata(String),
68
69 #[error("Storage error: {0}")]
70 Storage(String),
71
72 #[error("Wallet error: {0}")]
73 Wallet(String),
74
75 #[error("Signature error: {0}")]
76 Signature(String),
77
78 #[error("Encoding error: {0}")]
79 Encoding(String),
80
81 #[error("Subxt error: {0}")]
82 Subxt(Box<subxt::Error>),
83
84 #[error("Other error: {0}")]
85 Other(String),
86}
87
88impl From<subxt::Error> for Error {
89 fn from(err: subxt::Error) -> Self {
90 Error::Subxt(Box::new(err))
91 }
92}
93
94pub type Result<T> = std::result::Result<T, Error>;
96
97#[derive(Debug, Clone)]
99pub struct ChainConfig {
100 pub name: String,
102 pub endpoint: String,
104 pub ss58_prefix: u16,
106 pub token_symbol: String,
108 pub token_decimals: u8,
110}
111
112impl ChainConfig {
113 pub fn polkadot() -> Self {
115 Self {
116 name: "Polkadot".to_string(),
117 endpoint: "wss://rpc.polkadot.io".to_string(),
118 ss58_prefix: 0,
119 token_symbol: "DOT".to_string(),
120 token_decimals: 10,
121 }
122 }
123
124 pub fn kusama() -> Self {
126 Self {
127 name: "Kusama".to_string(),
128 endpoint: "wss://kusama-rpc.polkadot.io".to_string(),
129 ss58_prefix: 2,
130 token_symbol: "KSM".to_string(),
131 token_decimals: 12,
132 }
133 }
134
135 pub fn westend() -> Self {
137 Self {
138 name: "Westend".to_string(),
139 endpoint: "wss://westend-rpc.polkadot.io".to_string(),
140 ss58_prefix: 42,
141 token_symbol: "WND".to_string(),
142 token_decimals: 12,
143 }
144 }
145
146 pub fn paseo() -> Self {
148 Self {
149 name: "Paseo".to_string(),
150 endpoint: "wss://paseo.rpc.amforc.com".to_string(),
151 ss58_prefix: 42,
152 token_symbol: "PAS".to_string(),
153 token_decimals: 10,
154 }
155 }
156
157 pub fn custom(name: impl Into<String>, endpoint: impl Into<String>, ss58_prefix: u16) -> Self {
159 Self {
160 name: name.into(),
161 endpoint: endpoint.into(),
162 ss58_prefix,
163 token_symbol: "UNIT".to_string(),
164 token_decimals: 12,
165 }
166 }
167}
168
169pub struct SubstrateAdapter {
171 endpoint: String,
173 client: OnlineClient<PolkadotConfig>,
175 config: ChainConfig,
177 connected: bool,
179 metrics: Metrics,
181}
182
183impl SubstrateAdapter {
184 pub async fn connect(endpoint: &str) -> Result<Self> {
186 Self::connect_with_config(ChainConfig::custom("Substrate", endpoint, 42)).await
187 }
188
189 pub async fn connect_with_config(config: ChainConfig) -> Result<Self> {
191 info!("Connecting to {} at {}", config.name, config.endpoint);
192
193 let client = OnlineClient::<PolkadotConfig>::from_url(&config.endpoint)
195 .await
196 .map_err(|e| Error::Connection(format!("Failed to connect: {}", e)))?;
197
198 let _metadata = client.metadata();
200 debug!("Connected to {}", config.name);
201
202 Ok(Self {
203 endpoint: config.endpoint.clone(),
204 client,
205 config,
206 connected: true,
207 metrics: Metrics::new(),
208 })
209 }
210
211 pub fn client(&self) -> &OnlineClient<PolkadotConfig> {
213 &self.client
214 }
215
216 pub fn endpoint(&self) -> &str {
218 &self.endpoint
219 }
220
221 pub fn config(&self) -> &ChainConfig {
223 &self.config
224 }
225
226 pub fn is_connected(&self) -> bool {
228 self.connected
229 }
230
231 pub fn metrics(&self) -> MetricsSnapshot {
233 self.metrics.snapshot()
234 }
235
236 pub async fn get_transaction_status(&self, tx_hash: &str) -> Result<TransactionStatus> {
238 if !self.connected {
239 return Err(Error::Connection("Not connected".to_string()));
240 }
241
242 debug!("Getting transaction status for: {}", tx_hash);
243 self.metrics.record_rpc_call("get_transaction_status");
244
245 let hash_bytes = hex::decode(tx_hash.trim_start_matches("0x"))
247 .map_err(|e| Error::Transaction(format!("Invalid transaction hash: {}", e)))?;
248
249 if hash_bytes.len() != 32 {
250 return Err(Error::Transaction(
251 "Transaction hash must be 32 bytes".to_string(),
252 ));
253 }
254
255 let mut hash_array = [0u8; 32];
256 hash_array.copy_from_slice(&hash_bytes);
257
258 let latest_block = self
264 .client
265 .blocks()
266 .at_latest()
267 .await
268 .map_err(|e| Error::Connection(format!("Failed to get latest block: {}", e)))?;
269
270 let latest_number = latest_block.number();
271
272 let mut blocks_to_check = vec![];
274 let start_num = latest_number.saturating_sub(MAX_BLOCK_SEARCH_DEPTH);
275
276 let mut current_block = latest_block;
278 for _ in 0..MAX_BLOCK_SEARCH_DEPTH {
279 blocks_to_check.push((current_block.number(), current_block.hash()));
280
281 match current_block.header().parent_hash {
283 parent_hash if current_block.number() > start_num => {
284 match self.client.blocks().at(parent_hash).await {
285 Ok(parent) => current_block = parent,
286 Err(_) => break, }
288 }
289 _ => break,
290 }
291 }
292
293 for (block_num, block_hash) in blocks_to_check {
295 let block = self
296 .client
297 .blocks()
298 .at(block_hash)
299 .await
300 .map_err(|e| Error::Connection(format!("Failed to get block: {}", e)))?;
301
302 let extrinsics = block
304 .extrinsics()
305 .await
306 .map_err(|e| Error::Transaction(format!("Failed to get extrinsics: {}", e)))?;
307
308 for ext_details in extrinsics.iter() {
310 let ext_bytes = ext_details.bytes();
313 let computed_hash = sp_core::blake2_256(ext_bytes);
314
315 if computed_hash == hash_array {
316 let ext_index = ext_details.index();
318
319 let events = ext_details
321 .events()
322 .await
323 .map_err(|e| Error::Transaction(format!("Failed to get events: {}", e)))?;
324
325 let mut success = false;
326 let mut error_msg = None;
327
328 for event in events.iter() {
329 let event = event.map_err(|e| {
330 Error::Transaction(format!("Failed to decode event: {}", e))
331 })?;
332
333 if event.pallet_name() == "System" {
335 if event.variant_name() == "ExtrinsicSuccess" {
336 success = true;
337 } else if event.variant_name() == "ExtrinsicFailed" {
338 error_msg = Some(format!("Extrinsic {} failed", ext_index));
340 }
341 }
342 }
343
344 let confirmations = latest_number - block_num;
345
346 return if success {
347 if confirmations >= FINALIZATION_THRESHOLD {
349 Ok(TransactionStatus::Finalized {
350 block_hash: block_hash.to_string(),
351 block_number: block_num as u64,
352 })
353 } else {
354 Ok(TransactionStatus::Confirmed {
355 block_hash: block_hash.to_string(),
356 block_number: Some(block_num as u64),
357 })
358 }
359 } else if let Some(error) = error_msg {
360 Ok(TransactionStatus::Failed { error })
361 } else {
362 Ok(TransactionStatus::Unknown)
364 };
365 }
366 }
367 }
368
369 Ok(TransactionStatus::Unknown)
371 }
372
373 pub fn validate_address(&self, address: &Address) -> bool {
375 match address {
376 Address::Substrate(addr) => {
377 use sp_core::crypto::Ss58Codec;
379 sp_core::sr25519::Public::from_ss58check(addr).is_ok()
380 || sp_core::ed25519::Public::from_ss58check(addr).is_ok()
381 }
382 _ => false,
383 }
384 }
385
386 pub async fn get_balance(&self, address: &str) -> Result<u128> {
388 if !self.connected {
389 return Err(Error::Connection("Not connected".to_string()));
390 }
391
392 debug!("Getting balance for address: {}", address);
393 self.metrics.record_rpc_call("get_balance");
394
395 use sp_core::crypto::{AccountId32, Ss58Codec};
397 let account_id = AccountId32::from_ss58check(address)
398 .map_err(|e| Error::Storage(format!("Invalid SS58 address: {}", e)))?;
399
400 let account_bytes: &[u8] = account_id.as_ref();
402 let storage_query = subxt::dynamic::storage(
403 "System",
404 "Account",
405 vec![subxt::dynamic::Value::from_bytes(account_bytes)],
406 );
407
408 let result = self
409 .client
410 .storage()
411 .at_latest()
412 .await
413 .map_err(|e| Error::Storage(format!("Failed to get latest block: {}", e)))?
414 .fetch(&storage_query)
415 .await
416 .map_err(|e| Error::Storage(format!("Failed to query storage: {}", e)))?;
417
418 if let Some(account_data) = result {
419 let decoded = account_data
421 .to_value()
422 .map_err(|e| Error::Storage(format!("Failed to decode account data: {}", e)))?;
423
424 use subxt::dynamic::At as _;
427
428 let free_balance = decoded
429 .at("data")
430 .and_then(|data| data.at("free"))
431 .and_then(|free| free.as_u128())
432 .unwrap_or(0);
433
434 debug!("Balance for {}: {}", address, free_balance);
435 Ok(free_balance)
436 } else {
437 debug!("Account {} not found, returning 0 balance", address);
439 Ok(0)
440 }
441 }
442
443 pub async fn get_balance_formatted(&self, address: &str) -> Result<String> {
445 let balance = self.get_balance(address).await?;
446 let decimals = self.config.token_decimals as u32;
447 let divisor = if decimals <= 38 {
449 10u128.pow(decimals)
450 } else {
451 return Err(Error::Storage(format!(
452 "Token decimals too large: {}",
453 decimals
454 )));
455 };
456 let whole = balance / divisor;
457 let fraction = balance % divisor;
458
459 Ok(format!(
460 "{}.{:0width$} {}",
461 whole,
462 fraction,
463 self.config.token_symbol,
464 width = decimals as usize
465 ))
466 }
467
468 pub fn storage(&self) -> StorageClient {
470 StorageClient::new(self.client.clone(), self.metrics.clone())
471 }
472
473 pub fn transaction_executor(&self) -> TransactionExecutor {
475 TransactionExecutor::new(self.client.clone(), self.metrics.clone())
476 }
477
478 pub fn runtime_version(&self) -> u32 {
480 self.client.runtime_version().spec_version
481 }
482
483 pub fn chain_name(&self) -> &str {
485 &self.config.name
486 }
487}
488
489#[async_trait]
490impl apex_sdk_core::ChainAdapter for SubstrateAdapter {
491 async fn get_transaction_status(
492 &self,
493 tx_hash: &str,
494 ) -> std::result::Result<TransactionStatus, String> {
495 self.get_transaction_status(tx_hash)
496 .await
497 .map_err(|e| e.to_string())
498 }
499
500 fn validate_address(&self, address: &Address) -> bool {
501 self.validate_address(address)
502 }
503
504 fn chain_name(&self) -> &str {
505 self.chain_name()
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512
513 #[test]
514 fn test_chain_config() {
515 let polkadot = ChainConfig::polkadot();
516 assert_eq!(polkadot.name, "Polkadot");
517 assert_eq!(polkadot.ss58_prefix, 0);
518 assert_eq!(polkadot.token_symbol, "DOT");
519
520 let kusama = ChainConfig::kusama();
521 assert_eq!(kusama.name, "Kusama");
522 assert_eq!(kusama.ss58_prefix, 2);
523 assert_eq!(kusama.token_symbol, "KSM");
524
525 let paseo = ChainConfig::paseo();
526 assert_eq!(paseo.name, "Paseo");
527 assert_eq!(paseo.ss58_prefix, 42);
528 assert_eq!(paseo.token_symbol, "PAS");
529 assert_eq!(paseo.endpoint, "wss://paseo.rpc.amforc.com");
530 }
531
532 #[tokio::test]
533 #[ignore] async fn test_substrate_adapter_connect() {
535 let adapter = SubstrateAdapter::connect("wss://westend-rpc.polkadot.io").await;
536 assert!(adapter.is_ok());
537
538 let adapter = adapter.unwrap();
539 assert!(adapter.is_connected());
540 }
541
542 #[tokio::test]
543 #[ignore] async fn test_polkadot_connection() {
545 let adapter = SubstrateAdapter::connect_with_config(ChainConfig::polkadot()).await;
546 assert!(adapter.is_ok());
547 }
548
549 #[test]
550 fn test_address_validation() {
551 let valid_polkadot_addr = "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5";
553 let valid_kusama_addr = "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F";
554
555 assert!(!valid_polkadot_addr.is_empty());
557 assert!(!valid_kusama_addr.is_empty());
558 assert!(valid_polkadot_addr.chars().all(|c| c.is_alphanumeric()));
559 assert!(valid_kusama_addr.chars().all(|c| c.is_alphanumeric()));
560 }
561}