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) as u32;
321
322 return if success {
323 Ok(TransactionStatus::Confirmed {
324 block_number: block_num as u64,
325 confirmations,
326 })
327 } else if let Some(error) = error_msg {
328 Ok(TransactionStatus::Failed { error })
329 } else {
330 Ok(TransactionStatus::Unknown)
332 };
333 }
334 }
335 }
336
337 Ok(TransactionStatus::Unknown)
339 }
340
341 pub fn validate_address(&self, address: &Address) -> bool {
343 match address {
344 Address::Substrate(addr) => {
345 use sp_core::crypto::Ss58Codec;
347 sp_core::sr25519::Public::from_ss58check(addr).is_ok()
348 || sp_core::ed25519::Public::from_ss58check(addr).is_ok()
349 }
350 _ => false,
351 }
352 }
353
354 pub async fn get_balance(&self, address: &str) -> Result<u128> {
356 if !self.connected {
357 return Err(Error::Connection("Not connected".to_string()));
358 }
359
360 debug!("Getting balance for address: {}", address);
361 self.metrics.record_rpc_call("get_balance");
362
363 use sp_core::crypto::{AccountId32, Ss58Codec};
365 let account_id = AccountId32::from_ss58check(address)
366 .map_err(|e| Error::Storage(format!("Invalid SS58 address: {}", e)))?;
367
368 let account_bytes: &[u8] = account_id.as_ref();
370 let storage_query = subxt::dynamic::storage(
371 "System",
372 "Account",
373 vec![subxt::dynamic::Value::from_bytes(account_bytes)],
374 );
375
376 let result = self
377 .client
378 .storage()
379 .at_latest()
380 .await
381 .map_err(|e| Error::Storage(format!("Failed to get latest block: {}", e)))?
382 .fetch(&storage_query)
383 .await
384 .map_err(|e| Error::Storage(format!("Failed to query storage: {}", e)))?;
385
386 if let Some(account_data) = result {
387 let decoded = account_data
389 .to_value()
390 .map_err(|e| Error::Storage(format!("Failed to decode account data: {}", e)))?;
391
392 use subxt::dynamic::At as _;
395
396 let free_balance = decoded
397 .at("data")
398 .and_then(|data| data.at("free"))
399 .and_then(|free| free.as_u128())
400 .unwrap_or(0);
401
402 debug!("Balance for {}: {}", address, free_balance);
403 Ok(free_balance)
404 } else {
405 debug!("Account {} not found, returning 0 balance", address);
407 Ok(0)
408 }
409 }
410
411 pub async fn get_balance_formatted(&self, address: &str) -> Result<String> {
413 let balance = self.get_balance(address).await?;
414 let decimals = self.config.token_decimals as u32;
415 let divisor = if decimals <= 38 {
417 10u128.pow(decimals)
418 } else {
419 return Err(Error::Storage(format!(
420 "Token decimals too large: {}",
421 decimals
422 )));
423 };
424 let whole = balance / divisor;
425 let fraction = balance % divisor;
426
427 Ok(format!(
428 "{}.{:0width$} {}",
429 whole,
430 fraction,
431 self.config.token_symbol,
432 width = decimals as usize
433 ))
434 }
435
436 pub fn storage(&self) -> StorageClient {
438 StorageClient::new(self.client.clone(), self.metrics.clone())
439 }
440
441 pub fn transaction_executor(&self) -> TransactionExecutor {
443 TransactionExecutor::new(self.client.clone(), self.metrics.clone())
444 }
445
446 pub fn runtime_version(&self) -> u32 {
448 self.client.runtime_version().spec_version
449 }
450
451 pub fn chain_name(&self) -> &str {
453 &self.config.name
454 }
455}
456
457#[async_trait]
458impl apex_sdk_core::ChainAdapter for SubstrateAdapter {
459 async fn get_transaction_status(
460 &self,
461 tx_hash: &str,
462 ) -> std::result::Result<TransactionStatus, String> {
463 self.get_transaction_status(tx_hash)
464 .await
465 .map_err(|e| e.to_string())
466 }
467
468 fn validate_address(&self, address: &Address) -> bool {
469 self.validate_address(address)
470 }
471
472 fn chain_name(&self) -> &str {
473 self.chain_name()
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn test_chain_config() {
483 let polkadot = ChainConfig::polkadot();
484 assert_eq!(polkadot.name, "Polkadot");
485 assert_eq!(polkadot.ss58_prefix, 0);
486 assert_eq!(polkadot.token_symbol, "DOT");
487
488 let kusama = ChainConfig::kusama();
489 assert_eq!(kusama.name, "Kusama");
490 assert_eq!(kusama.ss58_prefix, 2);
491 assert_eq!(kusama.token_symbol, "KSM");
492 }
493
494 #[tokio::test]
495 #[ignore] async fn test_substrate_adapter_connect() {
497 let adapter = SubstrateAdapter::connect("wss://westend-rpc.polkadot.io").await;
498 assert!(adapter.is_ok());
499
500 let adapter = adapter.unwrap();
501 assert!(adapter.is_connected());
502 }
503
504 #[tokio::test]
505 #[ignore] async fn test_polkadot_connection() {
507 let adapter = SubstrateAdapter::connect_with_config(ChainConfig::polkadot()).await;
508 assert!(adapter.is_ok());
509 }
510
511 #[test]
512 fn test_address_validation() {
513 let valid_polkadot_addr = "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5";
516 let valid_kusama_addr = "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F";
517
518 assert!(!valid_polkadot_addr.is_empty());
521 assert!(!valid_kusama_addr.is_empty());
522 }
523}