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
50#[derive(Error, Debug)]
52pub enum Error {
53 #[error("Connection error: {0}")]
54 Connection(String),
55
56 #[error("Transaction error: {0}")]
57 Transaction(String),
58
59 #[error("Metadata error: {0}")]
60 Metadata(String),
61
62 #[error("Storage error: {0}")]
63 Storage(String),
64
65 #[error("Wallet error: {0}")]
66 Wallet(String),
67
68 #[error("Signature error: {0}")]
69 Signature(String),
70
71 #[error("Encoding error: {0}")]
72 Encoding(String),
73
74 #[error("Subxt error: {0}")]
75 Subxt(#[from] subxt::Error),
76
77 #[error("Other error: {0}")]
78 Other(String),
79}
80
81pub type Result<T> = std::result::Result<T, Error>;
83
84#[derive(Debug, Clone)]
86pub struct ChainConfig {
87 pub name: String,
89 pub endpoint: String,
91 pub ss58_prefix: u16,
93 pub token_symbol: String,
95 pub token_decimals: u8,
97}
98
99impl ChainConfig {
100 pub fn polkadot() -> Self {
102 Self {
103 name: "Polkadot".to_string(),
104 endpoint: "wss://rpc.polkadot.io".to_string(),
105 ss58_prefix: 0,
106 token_symbol: "DOT".to_string(),
107 token_decimals: 10,
108 }
109 }
110
111 pub fn kusama() -> Self {
113 Self {
114 name: "Kusama".to_string(),
115 endpoint: "wss://kusama-rpc.polkadot.io".to_string(),
116 ss58_prefix: 2,
117 token_symbol: "KSM".to_string(),
118 token_decimals: 12,
119 }
120 }
121
122 pub fn westend() -> Self {
124 Self {
125 name: "Westend".to_string(),
126 endpoint: "wss://westend-rpc.polkadot.io".to_string(),
127 ss58_prefix: 42,
128 token_symbol: "WND".to_string(),
129 token_decimals: 12,
130 }
131 }
132
133 pub fn custom(name: impl Into<String>, endpoint: impl Into<String>, ss58_prefix: u16) -> Self {
135 Self {
136 name: name.into(),
137 endpoint: endpoint.into(),
138 ss58_prefix,
139 token_symbol: "UNIT".to_string(),
140 token_decimals: 12,
141 }
142 }
143}
144
145pub struct SubstrateAdapter {
147 endpoint: String,
149 client: OnlineClient<PolkadotConfig>,
151 config: ChainConfig,
153 connected: bool,
155 metrics: Metrics,
157}
158
159impl SubstrateAdapter {
160 pub async fn connect(endpoint: &str) -> Result<Self> {
162 Self::connect_with_config(ChainConfig::custom("Substrate", endpoint, 42)).await
163 }
164
165 pub async fn connect_with_config(config: ChainConfig) -> Result<Self> {
167 info!("Connecting to {} at {}", config.name, config.endpoint);
168
169 let client = OnlineClient::<PolkadotConfig>::from_url(&config.endpoint)
171 .await
172 .map_err(|e| Error::Connection(format!("Failed to connect: {}", e)))?;
173
174 let _metadata = client.metadata();
176 debug!("Connected to {}", config.name);
177
178 Ok(Self {
179 endpoint: config.endpoint.clone(),
180 client,
181 config,
182 connected: true,
183 metrics: Metrics::new(),
184 })
185 }
186
187 pub fn client(&self) -> &OnlineClient<PolkadotConfig> {
189 &self.client
190 }
191
192 pub fn endpoint(&self) -> &str {
194 &self.endpoint
195 }
196
197 pub fn config(&self) -> &ChainConfig {
199 &self.config
200 }
201
202 pub fn is_connected(&self) -> bool {
204 self.connected
205 }
206
207 pub fn metrics(&self) -> MetricsSnapshot {
209 self.metrics.snapshot()
210 }
211
212 pub async fn get_transaction_status(&self, tx_hash: &str) -> Result<TransactionStatus> {
214 if !self.connected {
215 return Err(Error::Connection("Not connected".to_string()));
216 }
217
218 debug!("Getting transaction status for: {}", tx_hash);
219 self.metrics.record_rpc_call("get_transaction_status");
220
221 let hash_bytes = hex::decode(tx_hash.trim_start_matches("0x"))
223 .map_err(|e| Error::Transaction(format!("Invalid transaction hash: {}", e)))?;
224
225 if hash_bytes.len() != 32 {
226 return Err(Error::Transaction(
227 "Transaction hash must be 32 bytes".to_string(),
228 ));
229 }
230
231 let mut hash_array = [0u8; 32];
232 hash_array.copy_from_slice(&hash_bytes);
233
234 let latest_block = self
240 .client
241 .blocks()
242 .at_latest()
243 .await
244 .map_err(|e| Error::Connection(format!("Failed to get latest block: {}", e)))?;
245
246 let latest_number = latest_block.number();
247
248 let mut blocks_to_check = vec![];
250 let start_num = latest_number.saturating_sub(100);
251
252 let mut current_block = latest_block;
254 for _ in 0..100 {
255 blocks_to_check.push((current_block.number(), current_block.hash()));
256
257 match current_block.header().parent_hash {
259 parent_hash if current_block.number() > start_num => {
260 match self.client.blocks().at(parent_hash).await {
261 Ok(parent) => current_block = parent,
262 Err(_) => break, }
264 }
265 _ => break,
266 }
267 }
268
269 for (block_num, block_hash) in blocks_to_check {
271 let block = self
272 .client
273 .blocks()
274 .at(block_hash)
275 .await
276 .map_err(|e| Error::Connection(format!("Failed to get block: {}", e)))?;
277
278 let extrinsics = block
280 .extrinsics()
281 .await
282 .map_err(|e| Error::Transaction(format!("Failed to get extrinsics: {}", e)))?;
283
284 for ext_details in extrinsics.iter() {
286 let ext_bytes = ext_details.bytes();
289 let computed_hash = sp_core::blake2_256(ext_bytes);
290
291 if computed_hash == hash_array {
292 let ext_index = ext_details.index();
294
295 let events = ext_details
297 .events()
298 .await
299 .map_err(|e| Error::Transaction(format!("Failed to get events: {}", e)))?;
300
301 let mut success = false;
302 let mut error_msg = None;
303
304 for event in events.iter() {
305 let event = event.map_err(|e| {
306 Error::Transaction(format!("Failed to decode event: {}", e))
307 })?;
308
309 if event.pallet_name() == "System" {
311 if event.variant_name() == "ExtrinsicSuccess" {
312 success = true;
313 } else if event.variant_name() == "ExtrinsicFailed" {
314 error_msg = Some(format!("Extrinsic {} failed", ext_index));
316 }
317 }
318 }
319
320 let confirmations = latest_number - block_num;
321
322 return if success {
323 if confirmations >= 10 {
325 Ok(TransactionStatus::Finalized {
326 block_hash: block_hash.to_string(),
327 block_number: block_num as u64,
328 })
329 } else {
330 Ok(TransactionStatus::Confirmed {
331 block_hash: block_hash.to_string(),
332 block_number: Some(block_num as u64),
333 })
334 }
335 } else if let Some(error) = error_msg {
336 Ok(TransactionStatus::Failed { error })
337 } else {
338 Ok(TransactionStatus::Unknown)
340 };
341 }
342 }
343 }
344
345 Ok(TransactionStatus::Unknown)
347 }
348
349 pub fn validate_address(&self, address: &Address) -> bool {
351 match address {
352 Address::Substrate(addr) => {
353 use sp_core::crypto::Ss58Codec;
355 sp_core::sr25519::Public::from_ss58check(addr).is_ok()
356 || sp_core::ed25519::Public::from_ss58check(addr).is_ok()
357 }
358 _ => false,
359 }
360 }
361
362 pub async fn get_balance(&self, address: &str) -> Result<u128> {
364 if !self.connected {
365 return Err(Error::Connection("Not connected".to_string()));
366 }
367
368 debug!("Getting balance for address: {}", address);
369 self.metrics.record_rpc_call("get_balance");
370
371 use sp_core::crypto::{AccountId32, Ss58Codec};
373 let account_id = AccountId32::from_ss58check(address)
374 .map_err(|e| Error::Storage(format!("Invalid SS58 address: {}", e)))?;
375
376 let account_bytes: &[u8] = account_id.as_ref();
378 let storage_query = subxt::dynamic::storage(
379 "System",
380 "Account",
381 vec![subxt::dynamic::Value::from_bytes(account_bytes)],
382 );
383
384 let result = self
385 .client
386 .storage()
387 .at_latest()
388 .await
389 .map_err(|e| Error::Storage(format!("Failed to get latest block: {}", e)))?
390 .fetch(&storage_query)
391 .await
392 .map_err(|e| Error::Storage(format!("Failed to query storage: {}", e)))?;
393
394 if let Some(account_data) = result {
395 let decoded = account_data
397 .to_value()
398 .map_err(|e| Error::Storage(format!("Failed to decode account data: {}", e)))?;
399
400 use subxt::dynamic::At as _;
403
404 let free_balance = decoded
405 .at("data")
406 .and_then(|data| data.at("free"))
407 .and_then(|free| free.as_u128())
408 .unwrap_or(0);
409
410 debug!("Balance for {}: {}", address, free_balance);
411 Ok(free_balance)
412 } else {
413 debug!("Account {} not found, returning 0 balance", address);
415 Ok(0)
416 }
417 }
418
419 pub async fn get_balance_formatted(&self, address: &str) -> Result<String> {
421 let balance = self.get_balance(address).await?;
422 let decimals = self.config.token_decimals as u32;
423 let divisor = if decimals <= 38 {
425 10u128.pow(decimals)
426 } else {
427 return Err(Error::Storage(format!(
428 "Token decimals too large: {}",
429 decimals
430 )));
431 };
432 let whole = balance / divisor;
433 let fraction = balance % divisor;
434
435 Ok(format!(
436 "{}.{:0width$} {}",
437 whole,
438 fraction,
439 self.config.token_symbol,
440 width = decimals as usize
441 ))
442 }
443
444 pub fn storage(&self) -> StorageClient {
446 StorageClient::new(self.client.clone(), self.metrics.clone())
447 }
448
449 pub fn transaction_executor(&self) -> TransactionExecutor {
451 TransactionExecutor::new(self.client.clone(), self.metrics.clone())
452 }
453
454 pub fn runtime_version(&self) -> u32 {
456 self.client.runtime_version().spec_version
457 }
458
459 pub fn chain_name(&self) -> &str {
461 &self.config.name
462 }
463}
464
465#[async_trait]
466impl apex_sdk_core::ChainAdapter for SubstrateAdapter {
467 async fn get_transaction_status(
468 &self,
469 tx_hash: &str,
470 ) -> std::result::Result<TransactionStatus, String> {
471 self.get_transaction_status(tx_hash)
472 .await
473 .map_err(|e| e.to_string())
474 }
475
476 fn validate_address(&self, address: &Address) -> bool {
477 self.validate_address(address)
478 }
479
480 fn chain_name(&self) -> &str {
481 self.chain_name()
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 #[test]
490 fn test_chain_config() {
491 let polkadot = ChainConfig::polkadot();
492 assert_eq!(polkadot.name, "Polkadot");
493 assert_eq!(polkadot.ss58_prefix, 0);
494 assert_eq!(polkadot.token_symbol, "DOT");
495
496 let kusama = ChainConfig::kusama();
497 assert_eq!(kusama.name, "Kusama");
498 assert_eq!(kusama.ss58_prefix, 2);
499 assert_eq!(kusama.token_symbol, "KSM");
500 }
501
502 #[tokio::test]
503 #[ignore] async fn test_substrate_adapter_connect() {
505 let adapter = SubstrateAdapter::connect("wss://westend-rpc.polkadot.io").await;
506 assert!(adapter.is_ok());
507
508 let adapter = adapter.unwrap();
509 assert!(adapter.is_connected());
510 }
511
512 #[tokio::test]
513 #[ignore] async fn test_polkadot_connection() {
515 let adapter = SubstrateAdapter::connect_with_config(ChainConfig::polkadot()).await;
516 assert!(adapter.is_ok());
517 }
518
519 #[test]
520 fn test_address_validation() {
521 let valid_polkadot_addr = "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5";
524 let valid_kusama_addr = "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F";
525
526 assert!(!valid_polkadot_addr.is_empty());
529 assert!(!valid_kusama_addr.is_empty());
530 }
531}