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(any(
30 feature = "typed-polkadot",
31 feature = "typed-kusama",
32 feature = "typed-westend"
33))]
34pub mod metadata;
35
36pub use cache::{Cache, CacheConfig};
37pub use contracts::{
38 parse_metadata, ContractCallBuilder, ContractClient, ContractMetadata, GasLimit,
39 StorageDepositLimit,
40};
41pub use metrics::{Metrics, MetricsSnapshot};
42pub use pool::{ConnectionPool, PoolConfig};
43pub use signer::{ApexSigner, Ed25519Signer, Sr25519Signer};
44pub use storage::{StorageClient, StorageQuery};
45pub use transaction::{
46 BatchCall, BatchMode, ExtrinsicBuilder, FeeConfig, RetryConfig, TransactionExecutor,
47};
48pub use wallet::{KeyPairType, Wallet, WalletManager};
49pub use xcm::{
50 AssetId, Fungibility, Junction, MultiLocation, NetworkId, WeightLimit, XcmAsset, XcmConfig,
51 XcmExecutor, XcmTransferType, XcmVersion,
52};
53
54#[derive(Error, Debug)]
56pub enum Error {
57 #[error("Connection error: {0}")]
58 Connection(String),
59
60 #[error("Transaction error: {0}")]
61 Transaction(String),
62
63 #[error("Metadata error: {0}")]
64 Metadata(String),
65
66 #[error("Storage error: {0}")]
67 Storage(String),
68
69 #[error("Wallet error: {0}")]
70 Wallet(String),
71
72 #[error("Signature error: {0}")]
73 Signature(String),
74
75 #[error("Encoding error: {0}")]
76 Encoding(String),
77
78 #[error("Subxt error: {0}")]
79 Subxt(#[from] subxt::Error),
80
81 #[error("Other error: {0}")]
82 Other(String),
83}
84
85pub type Result<T> = std::result::Result<T, Error>;
87
88#[derive(Debug, Clone)]
90pub struct ChainConfig {
91 pub name: String,
93 pub endpoint: String,
95 pub ss58_prefix: u16,
97 pub token_symbol: String,
99 pub token_decimals: u8,
101}
102
103impl ChainConfig {
104 pub fn polkadot() -> Self {
106 Self {
107 name: "Polkadot".to_string(),
108 endpoint: "wss://rpc.polkadot.io".to_string(),
109 ss58_prefix: 0,
110 token_symbol: "DOT".to_string(),
111 token_decimals: 10,
112 }
113 }
114
115 pub fn kusama() -> Self {
117 Self {
118 name: "Kusama".to_string(),
119 endpoint: "wss://kusama-rpc.polkadot.io".to_string(),
120 ss58_prefix: 2,
121 token_symbol: "KSM".to_string(),
122 token_decimals: 12,
123 }
124 }
125
126 pub fn westend() -> Self {
128 Self {
129 name: "Westend".to_string(),
130 endpoint: "wss://westend-rpc.polkadot.io".to_string(),
131 ss58_prefix: 42,
132 token_symbol: "WND".to_string(),
133 token_decimals: 12,
134 }
135 }
136
137 pub fn custom(name: impl Into<String>, endpoint: impl Into<String>, ss58_prefix: u16) -> Self {
139 Self {
140 name: name.into(),
141 endpoint: endpoint.into(),
142 ss58_prefix,
143 token_symbol: "UNIT".to_string(),
144 token_decimals: 12,
145 }
146 }
147}
148
149pub struct SubstrateAdapter {
151 endpoint: String,
153 client: OnlineClient<PolkadotConfig>,
155 config: ChainConfig,
157 connected: bool,
159 metrics: Metrics,
161}
162
163impl SubstrateAdapter {
164 pub async fn connect(endpoint: &str) -> Result<Self> {
166 Self::connect_with_config(ChainConfig::custom("Substrate", endpoint, 42)).await
167 }
168
169 pub async fn connect_with_config(config: ChainConfig) -> Result<Self> {
171 info!("Connecting to {} at {}", config.name, config.endpoint);
172
173 let client = OnlineClient::<PolkadotConfig>::from_url(&config.endpoint)
175 .await
176 .map_err(|e| Error::Connection(format!("Failed to connect: {}", e)))?;
177
178 let _metadata = client.metadata();
180 debug!("Connected to {}", config.name);
181
182 Ok(Self {
183 endpoint: config.endpoint.clone(),
184 client,
185 config,
186 connected: true,
187 metrics: Metrics::new(),
188 })
189 }
190
191 pub fn client(&self) -> &OnlineClient<PolkadotConfig> {
193 &self.client
194 }
195
196 pub fn endpoint(&self) -> &str {
198 &self.endpoint
199 }
200
201 pub fn config(&self) -> &ChainConfig {
203 &self.config
204 }
205
206 pub fn is_connected(&self) -> bool {
208 self.connected
209 }
210
211 pub fn metrics(&self) -> MetricsSnapshot {
213 self.metrics.snapshot()
214 }
215
216 pub async fn get_transaction_status(&self, tx_hash: &str) -> Result<TransactionStatus> {
218 if !self.connected {
219 return Err(Error::Connection("Not connected".to_string()));
220 }
221
222 debug!("Getting transaction status for: {}", tx_hash);
223 self.metrics.record_rpc_call("get_transaction_status");
224
225 let hash_bytes = hex::decode(tx_hash.trim_start_matches("0x"))
227 .map_err(|e| Error::Transaction(format!("Invalid transaction hash: {}", e)))?;
228
229 if hash_bytes.len() != 32 {
230 return Err(Error::Transaction(
231 "Transaction hash must be 32 bytes".to_string(),
232 ));
233 }
234
235 let mut hash_array = [0u8; 32];
236 hash_array.copy_from_slice(&hash_bytes);
237
238 let latest_block = self
244 .client
245 .blocks()
246 .at_latest()
247 .await
248 .map_err(|e| Error::Connection(format!("Failed to get latest block: {}", e)))?;
249
250 let latest_number = latest_block.number();
251
252 let mut blocks_to_check = vec![];
254 let start_num = latest_number.saturating_sub(100);
255
256 let mut current_block = latest_block;
258 for _ in 0..100 {
259 blocks_to_check.push((current_block.number(), current_block.hash()));
260
261 match current_block.header().parent_hash {
263 parent_hash if current_block.number() > start_num => {
264 match self.client.blocks().at(parent_hash).await {
265 Ok(parent) => current_block = parent,
266 Err(_) => break, }
268 }
269 _ => break,
270 }
271 }
272
273 for (block_num, block_hash) in blocks_to_check {
275 let block = self
276 .client
277 .blocks()
278 .at(block_hash)
279 .await
280 .map_err(|e| Error::Connection(format!("Failed to get block: {}", e)))?;
281
282 let extrinsics = block
284 .extrinsics()
285 .await
286 .map_err(|e| Error::Transaction(format!("Failed to get extrinsics: {}", e)))?;
287
288 for ext_details in extrinsics.iter() {
290 let ext_bytes = ext_details.bytes();
293 let computed_hash = sp_core::blake2_256(ext_bytes);
294
295 if computed_hash == hash_array {
296 let ext_index = ext_details.index();
298
299 let events = ext_details
301 .events()
302 .await
303 .map_err(|e| Error::Transaction(format!("Failed to get events: {}", e)))?;
304
305 let mut success = false;
306 let mut error_msg = None;
307
308 for event in events.iter() {
309 let event = event.map_err(|e| {
310 Error::Transaction(format!("Failed to decode event: {}", e))
311 })?;
312
313 if event.pallet_name() == "System" {
315 if event.variant_name() == "ExtrinsicSuccess" {
316 success = true;
317 } else if event.variant_name() == "ExtrinsicFailed" {
318 error_msg = Some(format!("Extrinsic {} failed", ext_index));
320 }
321 }
322 }
323
324 let confirmations = latest_number - block_num;
325
326 return if success {
327 Ok(TransactionStatus::Confirmed {
328 block_number: block_num as u64,
329 confirmations,
330 })
331 } else if let Some(error) = error_msg {
332 Ok(TransactionStatus::Failed { error })
333 } else {
334 Ok(TransactionStatus::Unknown)
336 };
337 }
338 }
339 }
340
341 Ok(TransactionStatus::Unknown)
343 }
344
345 pub fn validate_address(&self, address: &Address) -> bool {
347 match address {
348 Address::Substrate(addr) => {
349 use sp_core::crypto::Ss58Codec;
351 sp_core::sr25519::Public::from_ss58check(addr).is_ok()
352 || sp_core::ed25519::Public::from_ss58check(addr).is_ok()
353 }
354 _ => false,
355 }
356 }
357
358 pub async fn get_balance(&self, address: &str) -> Result<u128> {
360 if !self.connected {
361 return Err(Error::Connection("Not connected".to_string()));
362 }
363
364 debug!("Getting balance for address: {}", address);
365 self.metrics.record_rpc_call("get_balance");
366
367 use sp_core::crypto::{AccountId32, Ss58Codec};
369 let account_id = AccountId32::from_ss58check(address)
370 .map_err(|e| Error::Storage(format!("Invalid SS58 address: {}", e)))?;
371
372 let account_bytes: &[u8] = account_id.as_ref();
374 let storage_query = subxt::dynamic::storage(
375 "System",
376 "Account",
377 vec![subxt::dynamic::Value::from_bytes(account_bytes)],
378 );
379
380 let result = self
381 .client
382 .storage()
383 .at_latest()
384 .await
385 .map_err(|e| Error::Storage(format!("Failed to get latest block: {}", e)))?
386 .fetch(&storage_query)
387 .await
388 .map_err(|e| Error::Storage(format!("Failed to query storage: {}", e)))?;
389
390 if let Some(account_data) = result {
391 let decoded = account_data
393 .to_value()
394 .map_err(|e| Error::Storage(format!("Failed to decode account data: {}", e)))?;
395
396 use subxt::dynamic::At as _;
399
400 let free_balance = decoded
401 .at("data")
402 .and_then(|data| data.at("free"))
403 .and_then(|free| free.as_u128())
404 .unwrap_or(0);
405
406 debug!("Balance for {}: {}", address, free_balance);
407 Ok(free_balance)
408 } else {
409 debug!("Account {} not found, returning 0 balance", address);
411 Ok(0)
412 }
413 }
414
415 pub async fn get_balance_formatted(&self, address: &str) -> Result<String> {
417 let balance = self.get_balance(address).await?;
418 let decimals = self.config.token_decimals as u32;
419 let divisor = if decimals <= 38 {
421 10u128.pow(decimals)
422 } else {
423 return Err(Error::Storage(format!(
424 "Token decimals too large: {}",
425 decimals
426 )));
427 };
428 let whole = balance / divisor;
429 let fraction = balance % divisor;
430
431 Ok(format!(
432 "{}.{:0width$} {}",
433 whole,
434 fraction,
435 self.config.token_symbol,
436 width = decimals as usize
437 ))
438 }
439
440 pub fn storage(&self) -> StorageClient {
442 StorageClient::new(self.client.clone(), self.metrics.clone())
443 }
444
445 pub fn transaction_executor(&self) -> TransactionExecutor {
447 TransactionExecutor::new(self.client.clone(), self.metrics.clone())
448 }
449
450 pub fn runtime_version(&self) -> u32 {
452 self.client.runtime_version().spec_version
453 }
454
455 pub fn chain_name(&self) -> &str {
457 &self.config.name
458 }
459}
460
461#[async_trait]
462impl apex_sdk_core::ChainAdapter for SubstrateAdapter {
463 async fn get_transaction_status(
464 &self,
465 tx_hash: &str,
466 ) -> std::result::Result<TransactionStatus, String> {
467 self.get_transaction_status(tx_hash)
468 .await
469 .map_err(|e| e.to_string())
470 }
471
472 fn validate_address(&self, address: &Address) -> bool {
473 self.validate_address(address)
474 }
475
476 fn chain_name(&self) -> &str {
477 self.chain_name()
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 #[test]
486 fn test_chain_config() {
487 let polkadot = ChainConfig::polkadot();
488 assert_eq!(polkadot.name, "Polkadot");
489 assert_eq!(polkadot.ss58_prefix, 0);
490 assert_eq!(polkadot.token_symbol, "DOT");
491
492 let kusama = ChainConfig::kusama();
493 assert_eq!(kusama.name, "Kusama");
494 assert_eq!(kusama.ss58_prefix, 2);
495 assert_eq!(kusama.token_symbol, "KSM");
496 }
497
498 #[tokio::test]
499 #[ignore] async fn test_substrate_adapter_connect() {
501 let adapter = SubstrateAdapter::connect("wss://westend-rpc.polkadot.io").await;
502 assert!(adapter.is_ok());
503
504 let adapter = adapter.unwrap();
505 assert!(adapter.is_connected());
506 }
507
508 #[tokio::test]
509 #[ignore] async fn test_polkadot_connection() {
511 let adapter = SubstrateAdapter::connect_with_config(ChainConfig::polkadot()).await;
512 assert!(adapter.is_ok());
513 }
514
515 #[test]
516 fn test_address_validation() {
517 let valid_polkadot_addr = "15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5";
520 let valid_kusama_addr = "HNZata7iMYWmk5RvZRTiAsSDhV8366zq2YGb3tLH5Upf74F";
521
522 assert!(!valid_polkadot_addr.is_empty());
525 assert!(!valid_kusama_addr.is_empty());
526 }
527}