Skip to main content

apex_sdk_types/
lib.rs

1//! # Apex SDK Types
2//!
3//! Common types and data structures used across the Apex SDK.
4//!
5//! This crate provides fundamental types for representing blockchain entities
6//! across different chain types (Substrate, EVM, Hybrid).
7//!
8//! ## Core Types
9//!
10//! - **Chain**: Enumeration of supported blockchain networks
11//! - **ChainType**: Classification of chains (Substrate, EVM, Hybrid)
12//! - **Address**: Generic address type supporting multiple formats
13//! - **TransactionStatus**: Unified transaction status representation
14//! - **CrossChainTransaction**: Cross-chain transaction information
15//!
16//! ## Example
17//!
18//! ```rust
19//! use apex_sdk_types::{Chain, ChainType, Address};
20//!
21//! // Create addresses for different chains
22//! let eth_addr = Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7");
23//! let dot_addr = Address::substrate("15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5");
24//!
25//! // Check chain types
26//! assert_eq!(Chain::Ethereum.chain_type(), ChainType::Evm);
27//! assert_eq!(Chain::Polkadot.chain_type(), ChainType::Substrate);
28//! assert_eq!(Chain::Moonbeam.chain_type(), ChainType::Hybrid);
29//! ```
30
31use blake2::Blake2b512;
32use serde::{Deserialize, Serialize};
33use sha3::{Digest, Keccak256};
34use thiserror::Error;
35
36/// Errors that can occur when working with addresses and chains
37#[derive(Error, Debug, Clone, PartialEq, Eq)]
38pub enum ValidationError {
39    /// Invalid EVM address format
40    #[error("Invalid EVM address format: {0}")]
41    InvalidEvmAddress(String),
42
43    /// EIP-55 checksum validation failed
44    #[error("EIP-55 checksum validation failed for address: {0}")]
45    InvalidChecksum(String),
46
47    /// Invalid chain ID
48    #[error("Invalid chain ID: expected {expected} for {chain}, got {actual}")]
49    InvalidChainId {
50        chain: String,
51        expected: u64,
52        actual: u64,
53    },
54
55    /// Chain ID not found for chain
56    #[error("Chain ID not available for chain: {0}")]
57    ChainIdNotFound(String),
58
59    /// Invalid Substrate SS58 address format
60    #[error("Invalid Substrate SS58 address format: {0}")]
61    InvalidSubstrateAddress(String),
62
63    /// SS58 checksum validation failed
64    #[error("SS58 checksum validation failed for address: {0}")]
65    InvalidSs58Checksum(String),
66}
67
68/// Blockchain types
69#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
70pub enum ChainType {
71    /// Substrate-based chain
72    Substrate,
73    /// EVM-based chain
74    Evm,
75    /// Hybrid chain (both Substrate and EVM)
76    Hybrid,
77}
78
79/// Supported blockchain networks
80#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
81pub enum Chain {
82    // Substrate Relay Chains
83    /// Polkadot relay chain
84    Polkadot,
85    /// Kusama relay chain
86    Kusama,
87
88    // Substrate Parachains
89    /// Moonbeam (Polkadot parachain with EVM)
90    Moonbeam,
91    /// Astar (Polkadot parachain with EVM)
92    Astar,
93    /// Acala DeFi Hub
94    Acala,
95    /// Phala Privacy Cloud
96    Phala,
97    /// Bifrost Liquid Staking
98    Bifrost,
99    /// Westend testnet
100    Westend,
101    /// Paseo testnet (default)
102    Paseo,
103
104    // EVM Layer 1
105    /// Ethereum mainnet
106    Ethereum,
107    /// Binance Smart Chain
108    BinanceSmartChain,
109    /// Polygon
110    Polygon,
111    /// Avalanche C-Chain
112    Avalanche,
113
114    // EVM Layer 2
115    /// Arbitrum One
116    Arbitrum,
117    /// Optimism
118    Optimism,
119    /// zkSync Era
120    ZkSync,
121    /// Base (Coinbase L2)
122    Base,
123}
124
125impl Chain {
126    pub fn chain_type(&self) -> ChainType {
127        match self {
128            // Pure Substrate chains
129            Chain::Polkadot
130            | Chain::Kusama
131            | Chain::Acala
132            | Chain::Phala
133            | Chain::Bifrost
134            | Chain::Westend
135            | Chain::Paseo => ChainType::Substrate,
136
137            // Pure EVM chains
138            Chain::Ethereum
139            | Chain::BinanceSmartChain
140            | Chain::Polygon
141            | Chain::Avalanche
142            | Chain::Arbitrum
143            | Chain::Optimism
144            | Chain::ZkSync
145            | Chain::Base => ChainType::Evm,
146
147            // Hybrid chains (Substrate + EVM)
148            Chain::Moonbeam | Chain::Astar => ChainType::Hybrid,
149        }
150    }
151
152    /// Get the chain name
153    pub fn name(&self) -> &str {
154        match self {
155            // Substrate
156            Chain::Polkadot => "Polkadot",
157            Chain::Kusama => "Kusama",
158            Chain::Acala => "Acala",
159            Chain::Phala => "Phala",
160            Chain::Bifrost => "Bifrost",
161            Chain::Westend => "Westend",
162            Chain::Paseo => "Paseo",
163
164            // EVM L1
165            Chain::Ethereum => "Ethereum",
166            Chain::BinanceSmartChain => "Binance Smart Chain",
167            Chain::Polygon => "Polygon",
168            Chain::Avalanche => "Avalanche",
169
170            // EVM L2
171            Chain::Arbitrum => "Arbitrum",
172            Chain::Optimism => "Optimism",
173            Chain::ZkSync => "zkSync",
174            Chain::Base => "Base",
175
176            // Hybrid
177            Chain::Moonbeam => "Moonbeam",
178            Chain::Astar => "Astar",
179        }
180    }
181
182    /// Get default RPC endpoint for the chain
183    pub fn default_endpoint(&self) -> &str {
184        match self {
185            // Substrate
186            Chain::Polkadot => "wss://polkadot.api.onfinality.io/public-ws",
187            Chain::Kusama => "wss://kusama.api.onfinality.io/public-ws",
188            Chain::Acala => "wss://acala.api.onfinality.io/public-ws",
189            Chain::Phala => "wss://phala.api.onfinality.io/public-ws",
190            Chain::Bifrost => "wss://bifrost-polkadot.api.onfinality.io/public-ws",
191            Chain::Westend => "wss://westend-rpc.polkadot.io",
192            Chain::Paseo => "wss://paseo.rpc.amforc.com",
193
194            // EVM L1
195            Chain::Ethereum => "https://eth.llamarpc.com",
196            Chain::BinanceSmartChain => "https://bsc.publicnode.com",
197            Chain::Polygon => "https://polygon-rpc.com",
198            Chain::Avalanche => "https://api.avax.network/ext/bc/C/rpc",
199
200            // EVM L2
201            Chain::Arbitrum => "https://arb1.arbitrum.io/rpc",
202            Chain::Optimism => "https://mainnet.optimism.io",
203            Chain::ZkSync => "https://mainnet.era.zksync.io",
204            Chain::Base => "https://mainnet.base.org",
205
206            // Hybrid
207            Chain::Moonbeam => "wss://moonbeam.api.onfinality.io/public-ws",
208            Chain::Astar => "wss://astar.api.onfinality.io/public-ws",
209        }
210    }
211
212    /// Get multiple RPC endpoints for reliability and failover
213    pub fn rpc_endpoints(&self) -> Vec<&str> {
214        match self {
215            // Substrate
216            Chain::Polkadot => vec![
217                "wss://polkadot.api.onfinality.io/public-ws",
218                "wss://rpc.ibp.network/polkadot",
219                "wss://polkadot.dotters.network",
220            ],
221            Chain::Kusama => vec![
222                "wss://kusama.api.onfinality.io/public-ws",
223                "wss://rpc.ibp.network/kusama",
224                "wss://kusama.dotters.network",
225            ],
226            Chain::Westend => vec![
227                "wss://westend-rpc.polkadot.io",
228                "wss://rpc.ibp.network/westend",
229                "wss://westend.dotters.network",
230            ],
231            // For other chains, return the single default endpoint
232            _ => vec![self.default_endpoint()],
233        }
234    }
235
236    /// Check if chain is a Layer 2 solution
237    pub fn is_layer2(&self) -> bool {
238        matches!(
239            self,
240            Chain::Arbitrum | Chain::Optimism | Chain::ZkSync | Chain::Base
241        )
242    }
243
244    /// Check if chain supports smart contracts
245    pub fn supports_smart_contracts(&self) -> bool {
246        match self.chain_type() {
247            ChainType::Evm => true,
248            ChainType::Hybrid => true,
249            ChainType::Substrate => matches!(
250                self,
251                Chain::Acala | Chain::Phala | Chain::Moonbeam | Chain::Astar
252            ),
253        }
254    }
255
256    /// Check if this is a testnet
257    pub fn is_testnet(&self) -> bool {
258        matches!(self, Chain::Westend | Chain::Paseo)
259    }
260
261    /// Parse chain from string (case-insensitive)
262    pub fn from_str_case_insensitive(s: &str) -> Option<Self> {
263        match s.to_lowercase().as_str() {
264            // Substrate
265            "polkadot" => Some(Chain::Polkadot),
266            "kusama" => Some(Chain::Kusama),
267            "acala" => Some(Chain::Acala),
268            "phala" => Some(Chain::Phala),
269            "bifrost" => Some(Chain::Bifrost),
270            "westend" => Some(Chain::Westend),
271            "paseo" => Some(Chain::Paseo),
272
273            // EVM L1
274            "ethereum" | "eth" => Some(Chain::Ethereum),
275            "binance" | "bsc" | "binancesmartchain" => Some(Chain::BinanceSmartChain),
276            "polygon" | "matic" => Some(Chain::Polygon),
277            "avalanche" | "avax" => Some(Chain::Avalanche),
278
279            // EVM L2
280            "arbitrum" | "arb" => Some(Chain::Arbitrum),
281            "optimism" | "op" => Some(Chain::Optimism),
282            "zksync" => Some(Chain::ZkSync),
283            "base" => Some(Chain::Base),
284
285            // Hybrid
286            "moonbeam" => Some(Chain::Moonbeam),
287            "astar" => Some(Chain::Astar),
288
289            _ => None,
290        }
291    }
292
293    /// Check if an endpoint URL is for Substrate (WebSocket-based)
294    pub fn is_substrate_endpoint(endpoint: &str) -> bool {
295        endpoint.starts_with("ws://") || endpoint.starts_with("wss://")
296    }
297
298    /// Check if an endpoint URL is for EVM (HTTP-based)
299    pub fn is_evm_endpoint(endpoint: &str) -> bool {
300        endpoint.starts_with("http://") || endpoint.starts_with("https://")
301    }
302
303    /// Get the chain ID for EVM-compatible chains
304    ///
305    /// Returns None for pure Substrate chains that don't have a chain ID concept.
306    /// For EVM and Hybrid chains, returns the standard EIP-155 chain ID.
307    pub fn chain_id(&self) -> Option<u64> {
308        match self {
309            // Pure Substrate chains don't have chain IDs
310            Chain::Polkadot
311            | Chain::Kusama
312            | Chain::Acala
313            | Chain::Phala
314            | Chain::Bifrost
315            | Chain::Westend
316            | Chain::Paseo => None,
317
318            // EVM L1 chains
319            Chain::Ethereum => Some(1),
320            Chain::BinanceSmartChain => Some(56),
321            Chain::Polygon => Some(137),
322            Chain::Avalanche => Some(43114),
323
324            // EVM L2 chains
325            Chain::Arbitrum => Some(42161),
326            Chain::Optimism => Some(10),
327            Chain::ZkSync => Some(324),
328            Chain::Base => Some(8453),
329
330            // Hybrid chains (EVM side)
331            Chain::Moonbeam => Some(1284),
332            Chain::Astar => Some(592),
333        }
334    }
335
336    /// Validate that a given chain ID matches this chain
337    ///
338    /// Returns an error if the chain ID doesn't match the expected value.
339    /// For Substrate-only chains, returns an error indicating chain ID is not applicable.
340    pub fn validate_chain_id(&self, chain_id: u64) -> Result<(), ValidationError> {
341        match self.chain_id() {
342            None => Err(ValidationError::ChainIdNotFound(self.name().to_string())),
343            Some(expected) => {
344                if expected == chain_id {
345                    Ok(())
346                } else {
347                    Err(ValidationError::InvalidChainId {
348                        chain: self.name().to_string(),
349                        expected,
350                        actual: chain_id,
351                    })
352                }
353            }
354        }
355    }
356}
357
358/// Validates an EVM address format (0x followed by 40 hex characters)
359fn is_valid_evm_format(addr: &str) -> bool {
360    if !addr.starts_with("0x") {
361        return false;
362    }
363
364    let hex_part = &addr[2..];
365    hex_part.len() == 40 && hex_part.chars().all(|c| c.is_ascii_hexdigit())
366}
367
368/// Computes EIP-55 checksum for an EVM address
369///
370/// EIP-55 uses keccak256 hash of the lowercase address to determine which
371/// characters should be uppercase in the checksummed version.
372fn to_checksum_address(addr: &str) -> String {
373    // Remove 0x prefix and convert to lowercase
374    let addr_lower = addr.trim_start_matches("0x").to_lowercase();
375
376    // Hash the lowercase address
377    let mut hasher = Keccak256::new();
378    hasher.update(addr_lower.as_bytes());
379    let hash = hasher.finalize();
380
381    // Build checksummed address
382    let mut result = String::from("0x");
383    for (i, ch) in addr_lower.chars().enumerate() {
384        if ch.is_ascii_digit() {
385            result.push(ch);
386        } else {
387            // If the i-th byte of the hash is >= 8, uppercase the character
388            let hash_byte = hash[i / 2];
389            let nibble = if i % 2 == 0 {
390                hash_byte >> 4
391            } else {
392                hash_byte & 0x0f
393            };
394
395            if nibble >= 8 {
396                result.push(ch.to_ascii_uppercase());
397            } else {
398                result.push(ch);
399            }
400        }
401    }
402
403    result
404}
405
406/// Validates EIP-55 checksum for an EVM address
407///
408/// If the address is all lowercase or all uppercase, it's considered valid
409/// (no checksum). Otherwise, it must match the EIP-55 checksum.
410fn validate_eip55_checksum(addr: &str) -> bool {
411    let hex_part = &addr[2..];
412
413    // If all lowercase or all uppercase, skip checksum validation
414    // (these are valid but non-checksummed addresses)
415    let all_lower = hex_part.chars().all(|c| !c.is_ascii_uppercase());
416    let all_upper = hex_part.chars().all(|c| !c.is_ascii_lowercase());
417
418    if all_lower || all_upper {
419        return true;
420    }
421
422    // Validate checksum
423    let checksummed = to_checksum_address(addr);
424    addr == checksummed
425}
426
427/// Validates a Substrate SS58 address format and checksum
428///
429/// SS58 is a modified Base58 encoding that includes:
430/// - A network identifier prefix
431/// - The account ID
432/// - A Blake2b checksum
433fn validate_ss58_address(addr: &str) -> bool {
434    // Decode the base58 string
435    let decoded = match bs58::decode(addr).into_vec() {
436        Ok(d) => d,
437        Err(_) => return false,
438    };
439
440    // SS58 addresses must be at least 3 bytes (1 prefix + 2 checksum)
441    if decoded.len() < 3 {
442        return false;
443    }
444
445    // Extract checksum (last 2 bytes)
446    let checksum_len =
447        if decoded.len() == 3 || decoded.len() == 4 || decoded.len() == 6 || decoded.len() == 10 {
448            1
449        } else {
450            2
451        };
452
453    let body_len = decoded.len() - checksum_len;
454    let body = &decoded[..body_len];
455    let checksum = &decoded[body_len..];
456
457    // Compute expected checksum using Blake2b
458    let mut hasher = Blake2b512::new();
459    hasher.update(b"SS58PRE");
460    hasher.update(body);
461    let hash = hasher.finalize();
462
463    // Compare checksums
464    &hash[..checksum_len] == checksum
465}
466
467/// Validates SS58 format with specific network ID
468fn validate_ss58_for_network(addr: &str, expected_ss58_format: u16) -> bool {
469    if !validate_ss58_address(addr) {
470        return false;
471    }
472
473    // Decode and extract network identifier
474    let decoded = match bs58::decode(addr).into_vec() {
475        Ok(d) => d,
476        Err(_) => return false,
477    };
478
479    if decoded.is_empty() {
480        return false;
481    }
482
483    // Extract SS58 format from first byte(s)
484    let network_id = if decoded[0] & 0b01000000 == 0 {
485        // Simple format: 6-bit network ID (0-63)
486        u16::from(decoded[0] & 0b00111111)
487    } else {
488        // Extended format: 14-bit network ID
489        // First byte: 01xxxxxx (6 bits, lower part)
490        // Second byte: xxxxxxxx (8 bits, upper part)
491        // Network ID = ((first_byte & 0x3F) << 2) | ((second_byte & 0xFC) >> 2) | 0x0040
492        if decoded.len() < 2 {
493            return false;
494        }
495        let lower = u16::from(decoded[0] & 0b00111111);
496        let upper = u16::from(decoded[1]);
497        // Combine to form 14-bit ID: lower 6 bits from first byte, upper 8 bits from second byte
498        ((lower << 8) | upper) | 0x0040
499    };
500
501    network_id == expected_ss58_format
502}
503
504/// Extract SS58 network prefix from an address string
505/// Returns None if the address cannot be decoded
506pub fn extract_ss58_prefix(address: &str) -> Option<u16> {
507    use bs58;
508
509    // Decode the base58 string
510    let decoded = match bs58::decode(address).into_vec() {
511        Ok(bytes) => bytes,
512        Err(_) => return None,
513    };
514
515    if decoded.is_empty() {
516        return None;
517    }
518
519    // Extract SS58 format from first byte(s)
520    let network_id = if decoded[0] & 0b01000000 == 0 {
521        // Simple format: 6-bit network ID (0-63)
522        u16::from(decoded[0] & 0b00111111)
523    } else {
524        // Extended format: 14-bit network ID
525        if decoded.len() < 2 {
526            return None;
527        }
528        let lower = u16::from(decoded[0] & 0b00111111);
529        let upper = u16::from(decoded[1]);
530        ((lower << 8) | upper) | 0x0040
531    };
532
533    Some(network_id)
534}
535
536/// Generic address type for different chains
537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
538pub enum Address {
539    /// Substrate SS58 address
540    Substrate(String),
541    /// EVM hex address (0x...) - validated with EIP-55 checksum
542    Evm(String),
543}
544
545impl Address {
546    /// Create a Substrate address with validation
547    ///
548    /// This function validates:
549    /// - SS58 base58 encoding
550    /// - Blake2b checksum
551    ///
552    /// # Errors
553    ///
554    /// Returns `Err` if the address format is invalid or checksum validation fails.
555    ///
556    /// # Example
557    ///
558    /// ```
559    /// use apex_sdk_types::Address;
560    ///
561    /// // Valid SS58 address (Polkadot)
562    /// let addr = Address::substrate_checked("15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5").unwrap();
563    ///
564    /// // Invalid address
565    /// let result = Address::substrate_checked("invalid");
566    /// assert!(result.is_err());
567    /// ```
568    pub fn substrate_checked(addr: impl Into<String>) -> Result<Self, ValidationError> {
569        let addr_str = addr.into();
570
571        // Validate SS58 format and checksum
572        if !validate_ss58_address(&addr_str) {
573            return Err(ValidationError::InvalidSubstrateAddress(addr_str));
574        }
575
576        Ok(Address::Substrate(addr_str))
577    }
578
579    /// Create a Substrate address without validation (legacy method)
580    ///
581    /// **Warning**: This method does not perform SS58 checksum validation.
582    /// Use `substrate_checked()` instead for new code.
583    ///
584    /// This method is provided for backward compatibility and cases where
585    /// validation is not required (e.g., trusted input sources).
586    pub fn substrate(addr: impl Into<String>) -> Self {
587        Address::Substrate(addr.into())
588    }
589
590    /// Create an EVM address with validation
591    ///
592    /// This function validates:
593    /// - Address format (0x followed by 40 hex characters)
594    /// - EIP-55 checksum (if the address has mixed case)
595    ///
596    /// # Errors
597    ///
598    /// Returns `Err` if the address format is invalid or checksum validation fails.
599    ///
600    /// # Example
601    ///
602    /// ```
603    /// use apex_sdk_types::Address;
604    ///
605    /// // Valid checksummed address
606    /// let addr = Address::evm_checked("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed").unwrap();
607    ///
608    /// // Valid lowercase address (no checksum)
609    /// let addr = Address::evm_checked("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed").unwrap();
610    ///
611    /// // Invalid checksum
612    /// let result = Address::evm_checked("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAeD");
613    /// assert!(result.is_err());
614    /// ```
615    pub fn evm_checked(addr: impl Into<String>) -> Result<Self, ValidationError> {
616        let addr_str = addr.into();
617
618        // Validate format
619        if !is_valid_evm_format(&addr_str) {
620            return Err(ValidationError::InvalidEvmAddress(addr_str));
621        }
622
623        // Validate EIP-55 checksum
624        if !validate_eip55_checksum(&addr_str) {
625            return Err(ValidationError::InvalidChecksum(addr_str));
626        }
627
628        Ok(Address::Evm(addr_str))
629    }
630
631    /// Create an EVM address without validation (legacy method)
632    ///
633    /// **Warning**: This method does not perform EIP-55 checksum validation.
634    /// Use `evm_checked()` instead for new code.
635    ///
636    /// This method is provided for backward compatibility and cases where
637    /// validation is not required (e.g., trusted input sources).
638    pub fn evm(addr: impl Into<String>) -> Self {
639        Address::Evm(addr.into())
640    }
641
642    /// Create a Substrate address with network-specific validation
643    ///
644    /// This function validates SS58 format and ensures the address
645    /// belongs to the specified network.
646    ///
647    /// # Errors
648    ///
649    /// Returns `Err` if the address format is invalid or doesn't match the expected network.
650    pub fn substrate_for_chain(
651        addr: impl Into<String>,
652        chain: &Chain,
653    ) -> Result<Self, ValidationError> {
654        let addr_str = addr.into();
655
656        let expected_ss58_format = match chain {
657            Chain::Polkadot => 0,
658            Chain::Kusama => 2,
659            Chain::Westend => 42,
660            Chain::Paseo => 42, // Same as Westend
661            Chain::Moonbeam => 1284,
662            Chain::Astar => 5,
663            Chain::Acala => 10,
664            Chain::Phala => 30,
665            Chain::Bifrost => 6,
666            _ => return Err(ValidationError::ChainIdNotFound(chain.name().to_string())),
667        };
668
669        if !validate_ss58_for_network(&addr_str, expected_ss58_format) {
670            return Err(ValidationError::InvalidSs58Checksum(format!(
671                "Address {} is not valid for network {} (expected SS58 format {})",
672                addr_str,
673                chain.name(),
674                expected_ss58_format
675            )));
676        }
677
678        Ok(Address::Substrate(addr_str))
679    }
680
681    /// Convert EVM address to checksummed format
682    ///
683    /// For EVM addresses, returns the EIP-55 checksummed version.
684    /// For Substrate addresses, returns the address unchanged.
685    pub fn to_checksum(&self) -> String {
686        match self {
687            Address::Evm(addr) => to_checksum_address(addr),
688            Address::Substrate(addr) => addr.clone(),
689        }
690    }
691
692    pub fn as_str(&self) -> &str {
693        match self {
694            Address::Substrate(s) | Address::Evm(s) => s,
695        }
696    }
697
698    /// Validate the address format and checksum
699    ///
700    /// For EVM addresses, validates EIP-55 checksum.
701    /// For Substrate addresses, validates SS58 format.
702    pub fn validate(&self) -> Result<(), ValidationError> {
703        match self {
704            Address::Evm(addr) => {
705                if !is_valid_evm_format(addr) {
706                    return Err(ValidationError::InvalidEvmAddress(addr.clone()));
707                }
708                if !validate_eip55_checksum(addr) {
709                    return Err(ValidationError::InvalidChecksum(addr.clone()));
710                }
711                Ok(())
712            }
713            Address::Substrate(addr) => {
714                if !validate_ss58_address(addr) {
715                    return Err(ValidationError::InvalidSubstrateAddress(addr.clone()));
716                }
717                Ok(())
718            }
719        }
720    }
721}
722
723impl std::fmt::Display for Address {
724    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725        match self {
726            Address::Substrate(addr) => write!(f, "{}", addr),
727            Address::Evm(addr) => write!(f, "{}", addr),
728        }
729    }
730}
731
732/// Transaction status
733#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
734pub enum TxStatus {
735    /// Transaction is pending
736    Pending,
737    /// Transaction is in mempool
738    InMempool,
739    /// Transaction is confirmed
740    Confirmed,
741    /// Transaction is finalized
742    Finalized,
743    /// Transaction failed
744    Failed,
745    /// Transaction status unknown
746    Unknown,
747}
748
749/// Comprehensive transaction status information
750#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
751pub struct TransactionStatus {
752    /// Transaction hash
753    pub hash: String,
754    /// Current status
755    pub status: TxStatus,
756    /// Block number where transaction was included
757    pub block_number: Option<u64>,
758    /// Block hash
759    pub block_hash: Option<String>,
760    /// Gas used by the transaction
761    pub gas_used: Option<u64>,
762    /// Effective gas price
763    pub effective_gas_price: Option<u128>,
764    /// Number of confirmations
765    pub confirmations: Option<u32>,
766    /// Error message (if status is Failed)
767    pub error: Option<String>,
768}
769
770impl TransactionStatus {
771    pub fn pending(hash: String) -> Self {
772        Self {
773            hash,
774            status: TxStatus::Pending,
775            block_number: None,
776            block_hash: None,
777            gas_used: None,
778            effective_gas_price: None,
779            confirmations: None,
780            error: None,
781        }
782    }
783
784    /// Create a new confirmed transaction status
785    pub fn confirmed(
786        hash: String,
787        block_number: u64,
788        block_hash: String,
789        gas_used: Option<u64>,
790        effective_gas_price: Option<u128>,
791        confirmations: Option<u32>,
792    ) -> Self {
793        Self {
794            hash,
795            status: TxStatus::Confirmed,
796            block_number: Some(block_number),
797            block_hash: Some(block_hash),
798            gas_used,
799            effective_gas_price,
800            confirmations,
801            error: None,
802        }
803    }
804
805    /// Create a new finalized transaction status
806    pub fn finalized(
807        hash: String,
808        block_number: u64,
809        block_hash: String,
810        gas_used: Option<u64>,
811        effective_gas_price: Option<u128>,
812        confirmations: Option<u32>,
813    ) -> Self {
814        Self {
815            hash,
816            status: TxStatus::Finalized,
817            block_number: Some(block_number),
818            block_hash: Some(block_hash),
819            gas_used,
820            effective_gas_price,
821            confirmations,
822            error: None,
823        }
824    }
825
826    /// Create a new failed transaction status
827    pub fn failed(hash: String, error: String) -> Self {
828        Self {
829            hash,
830            status: TxStatus::Failed,
831            block_number: None,
832            block_hash: None,
833            gas_used: None,
834            effective_gas_price: None,
835            confirmations: None,
836            error: Some(error),
837        }
838    }
839
840    pub fn unknown(hash: String) -> Self {
841        Self {
842            hash,
843            status: TxStatus::Unknown,
844            block_number: None,
845            block_hash: None,
846            gas_used: None,
847            effective_gas_price: None,
848            confirmations: None,
849            error: None,
850        }
851    }
852
853    /// Check if transaction is confirmed or finalized
854    pub fn is_confirmed(&self) -> bool {
855        matches!(self.status, TxStatus::Confirmed | TxStatus::Finalized)
856    }
857
858    /// Check if transaction is finalized
859    pub fn is_finalized(&self) -> bool {
860        matches!(self.status, TxStatus::Finalized)
861    }
862}
863
864impl Default for TransactionStatus {
865    fn default() -> Self {
866        Self {
867            hash: String::new(),
868            status: TxStatus::Unknown,
869            block_number: None,
870            block_hash: None,
871            gas_used: None,
872            effective_gas_price: None,
873            confirmations: None,
874            error: None,
875        }
876    }
877}
878
879// Backward compatibility alias
880pub use TransactionStatus as OldTransactionStatus;
881
882/// Represents a blockchain event emitted by a smart contract or runtime.
883///
884/// The `Event` struct captures details about an event, including its name, associated data,
885/// the block and transaction in which it occurred, and its index within the block.
886///
887/// # Fields
888/// - `name`: The name of the event (e.g., `"Transfer"`, `"Approval"`).
889/// - `data`: The event payload as a JSON value. This typically contains event parameters.
890/// - `block_number`: The block number in which the event was emitted, if available.
891/// - `tx_hash`: The transaction hash associated with the event, if available.
892/// - `index`: The index of the event within the block, if available.
893///
894/// # Example
895/// ```
896/// use apex_sdk_types::Event;
897/// use serde_json::json;
898///
899/// let event = Event {
900///     name: "Transfer".to_string(),
901///     data: json!({
902///         "from": "0x123...",
903///         "to": "0x456...",
904///         "value": 1000
905///     }),
906///     block_number: Some(123456),
907///     tx_hash: Some("0xabc...".to_string()),
908///     index: Some(0),
909/// };
910/// assert_eq!(event.name, "Transfer");
911/// ```
912#[derive(Debug, Clone, Serialize, Deserialize)]
913pub struct Event {
914    /// The name of the event (e.g., "Transfer", "Approval").
915    pub name: String,
916    /// The event payload as a JSON value, typically containing event parameters.
917    pub data: serde_json::Value,
918    /// The block number in which the event was emitted, if available.
919    pub block_number: Option<u64>,
920    /// The transaction hash associated with the event, if available.
921    pub tx_hash: Option<String>,
922    /// The index of the event within the block, if available.
923    pub index: Option<u32>,
924}
925
926/// Filter criteria for subscribing to blockchain events.
927///
928/// This struct allows you to specify which events to receive by name, contract address,
929/// and block range. All fields are optional; if a field is `None`, it will not be used
930/// as a filter criterion.
931#[derive(Debug, Clone, Serialize, Deserialize)]
932pub struct EventFilter {
933    /// List of event names to filter for.
934    ///
935    /// If specified, only events with names matching one of the strings in this list
936    /// will be included. If `None`, all event names are included.
937    pub event_names: Option<Vec<String>>,
938    /// List of contract addresses to filter for.
939    ///
940    /// If specified, only events emitted by contracts with addresses in this list
941    /// will be included. If `None`, events from all addresses are included.
942    pub addresses: Option<Vec<Address>>,
943    /// The starting block number (inclusive) for filtering events.
944    ///
945    /// If specified, only events from blocks with number greater than or equal to this
946    /// value will be included. If `None`, events from all blocks are included.
947    pub from_block: Option<u64>,
948    /// The ending block number (inclusive) for filtering events.
949    ///
950    /// If specified, only events from blocks with number less than or equal to this
951    /// value will be included. If `None`, events up to the latest block are included.
952    pub to_block: Option<u64>,
953}
954
955/// Cross-chain transaction info
956#[derive(Debug, Clone, Serialize, Deserialize)]
957pub struct CrossChainTransaction {
958    /// Transaction ID
959    pub id: String,
960    /// Source chain
961    pub source_chain: Chain,
962    /// Destination chain
963    pub destination_chain: Chain,
964    /// Source transaction hash
965    pub source_tx_hash: Option<String>,
966    /// Destination transaction hash
967    pub destination_tx_hash: Option<String>,
968    /// Transaction status
969    pub status: TransactionStatus,
970    /// Timestamp
971    pub timestamp: u64,
972}
973
974#[cfg(test)]
975mod tests {
976    use super::*;
977
978    #[test]
979    fn test_chain_type() {
980        assert_eq!(Chain::Polkadot.chain_type(), ChainType::Substrate);
981        assert_eq!(Chain::Ethereum.chain_type(), ChainType::Evm);
982        assert_eq!(Chain::Moonbeam.chain_type(), ChainType::Hybrid);
983    }
984
985    #[test]
986    fn test_address_creation() {
987        let sub_addr = Address::substrate("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY");
988        assert!(matches!(sub_addr, Address::Substrate(_)));
989
990        let evm_addr = Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7");
991        assert!(matches!(evm_addr, Address::Evm(_)));
992    }
993
994    #[test]
995    fn test_chain_id() {
996        // EVM chains should have chain IDs
997        assert_eq!(Chain::Ethereum.chain_id(), Some(1));
998        assert_eq!(Chain::BinanceSmartChain.chain_id(), Some(56));
999        assert_eq!(Chain::Polygon.chain_id(), Some(137));
1000        assert_eq!(Chain::Avalanche.chain_id(), Some(43114));
1001        assert_eq!(Chain::Arbitrum.chain_id(), Some(42161));
1002        assert_eq!(Chain::Optimism.chain_id(), Some(10));
1003        assert_eq!(Chain::ZkSync.chain_id(), Some(324));
1004        assert_eq!(Chain::Base.chain_id(), Some(8453));
1005
1006        // Hybrid chains should have chain IDs (EVM side)
1007        assert_eq!(Chain::Moonbeam.chain_id(), Some(1284));
1008        assert_eq!(Chain::Astar.chain_id(), Some(592));
1009
1010        // Substrate chains should not have chain IDs
1011        assert_eq!(Chain::Polkadot.chain_id(), None);
1012        assert_eq!(Chain::Kusama.chain_id(), None);
1013        assert_eq!(Chain::Westend.chain_id(), None);
1014        assert_eq!(Chain::Paseo.chain_id(), None);
1015    }
1016
1017    #[test]
1018    fn test_chain_id_validation() {
1019        // Valid chain IDs
1020        assert!(Chain::Ethereum.validate_chain_id(1).is_ok());
1021        assert!(Chain::BinanceSmartChain.validate_chain_id(56).is_ok());
1022        assert!(Chain::Polygon.validate_chain_id(137).is_ok());
1023
1024        // Invalid chain IDs
1025        assert!(Chain::Ethereum.validate_chain_id(56).is_err());
1026        assert!(Chain::Polygon.validate_chain_id(1).is_err());
1027
1028        // Substrate chains should return error
1029        assert!(Chain::Polkadot.validate_chain_id(1).is_err());
1030    }
1031
1032    #[test]
1033    fn test_eip55_valid_checksummed_addresses() {
1034        let valid_addresses = vec![
1035            "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
1036            "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
1037            "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
1038            "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb",
1039        ];
1040
1041        for addr in valid_addresses {
1042            let result = Address::evm_checked(addr);
1043            assert!(
1044                result.is_ok(),
1045                "Address {} should be valid, got error: {:?}",
1046                addr,
1047                result.err()
1048            );
1049        }
1050    }
1051
1052    #[test]
1053    fn test_eip55_lowercase_addresses() {
1054        // All lowercase addresses are valid (no checksum)
1055        let lowercase_addr = "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed";
1056        assert!(Address::evm_checked(lowercase_addr).is_ok());
1057
1058        let lowercase_addr2 = "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359";
1059        assert!(Address::evm_checked(lowercase_addr2).is_ok());
1060    }
1061
1062    #[test]
1063    fn test_eip55_uppercase_addresses() {
1064        // All uppercase addresses are valid (no checksum)
1065        let uppercase_addr = "0x5AAEB6053F3E94C9B9A09F33669435E7EF1BEAED";
1066        assert!(Address::evm_checked(uppercase_addr).is_ok());
1067    }
1068
1069    #[test]
1070    fn test_eip55_invalid_checksum() {
1071        // Invalid checksum (last character changed from 'd' to 'D')
1072        let invalid_addr = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAeD";
1073        let result = Address::evm_checked(invalid_addr);
1074        assert!(result.is_err());
1075        assert!(matches!(
1076            result.unwrap_err(),
1077            ValidationError::InvalidChecksum(_)
1078        ));
1079    }
1080
1081    #[test]
1082    fn test_eip55_invalid_format() {
1083        // Missing 0x prefix
1084        let no_prefix = "5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed";
1085        let result = Address::evm_checked(no_prefix);
1086        assert!(result.is_err());
1087        assert!(matches!(
1088            result.unwrap_err(),
1089            ValidationError::InvalidEvmAddress(_)
1090        ));
1091
1092        // Too short
1093        let too_short = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeA";
1094        let result = Address::evm_checked(too_short);
1095        assert!(result.is_err());
1096
1097        // Too long
1098        let too_long = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAedAA";
1099        let result = Address::evm_checked(too_long);
1100        assert!(result.is_err());
1101
1102        // Invalid hex characters
1103        let invalid_hex = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAeG";
1104        let result = Address::evm_checked(invalid_hex);
1105        assert!(result.is_err());
1106    }
1107
1108    #[test]
1109    fn test_to_checksum_address() {
1110        let lowercase = "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed";
1111        let checksummed = to_checksum_address(lowercase);
1112        assert_eq!(checksummed, "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed");
1113
1114        let lowercase2 = "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359";
1115        let checksummed2 = to_checksum_address(lowercase2);
1116        assert_eq!(checksummed2, "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359");
1117    }
1118
1119    #[test]
1120    fn test_address_to_checksum_method() {
1121        let addr = Address::evm("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed");
1122        assert_eq!(
1123            addr.to_checksum(),
1124            "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
1125        );
1126
1127        // Substrate addresses should be unchanged
1128        let sub_addr = Address::substrate("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY");
1129        assert_eq!(
1130            sub_addr.to_checksum(),
1131            "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
1132        );
1133    }
1134
1135    #[test]
1136    fn test_address_validate() {
1137        // Valid checksummed EVM address
1138        let addr = Address::evm("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed");
1139        assert!(addr.validate().is_ok());
1140
1141        // Valid lowercase EVM address
1142        let addr = Address::evm("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed");
1143        assert!(addr.validate().is_ok());
1144
1145        // Invalid checksum
1146        let addr = Address::evm("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAeD");
1147        assert!(addr.validate().is_err());
1148
1149        // Invalid format
1150        let addr = Address::evm("invalid");
1151        assert!(addr.validate().is_err());
1152
1153        // Valid Substrate address
1154        let addr = Address::substrate("15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5");
1155        assert!(addr.validate().is_ok());
1156
1157        // Invalid Substrate address
1158        let addr = Address::substrate("invalid");
1159        assert!(addr.validate().is_err());
1160    }
1161
1162    #[test]
1163    fn test_is_testnet() {
1164        assert!(Chain::Westend.is_testnet());
1165        assert!(Chain::Paseo.is_testnet());
1166        assert!(!Chain::Polkadot.is_testnet());
1167        assert!(!Chain::Ethereum.is_testnet());
1168    }
1169
1170    #[test]
1171    fn test_substrate_ss58_validation_valid_addresses() {
1172        // Valid SS58 addresses (Polkadot format)
1173        let valid_addresses = vec![
1174            "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
1175            "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
1176            "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM",
1177        ];
1178
1179        for addr in valid_addresses {
1180            let result = Address::substrate_checked(addr);
1181            assert!(
1182                result.is_ok(),
1183                "Address {} should be valid, got error: {:?}",
1184                addr,
1185                result.err()
1186            );
1187        }
1188    }
1189
1190    #[test]
1191    fn test_substrate_ss58_validation_invalid_addresses() {
1192        // Invalid SS58 addresses
1193        let invalid_addresses = vec![
1194            "invalid",                                             // Not base58
1195            "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQ",     // Too short
1196            "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY123", // Too long
1197            "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQX",    // Invalid checksum
1198        ];
1199
1200        for addr in invalid_addresses {
1201            let result = Address::substrate_checked(addr);
1202            assert!(
1203                result.is_err(),
1204                "Address {} should be invalid but was accepted",
1205                addr
1206            );
1207        }
1208    }
1209
1210    #[test]
1211    fn test_substrate_ss58_validation_error_types() {
1212        let invalid_format = "not-base58!@#";
1213        let result = Address::substrate_checked(invalid_format);
1214        assert!(result.is_err());
1215        match result.unwrap_err() {
1216            ValidationError::InvalidSubstrateAddress(_) => (),
1217            _ => panic!("Expected InvalidSubstrateAddress error"),
1218        }
1219
1220        let invalid_checksum = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQX";
1221        let result = Address::substrate_checked(invalid_checksum);
1222        assert!(result.is_err());
1223        // The error might be InvalidSubstrateAddress since the checksum validation fails
1224    }
1225
1226    #[test]
1227    fn test_validation_error_messages() {
1228        let invalid_evm = "0xinvalid";
1229        let result = Address::evm_checked(invalid_evm);
1230        assert!(result.is_err());
1231        let err = result.unwrap_err();
1232        assert!(err.to_string().contains("Invalid EVM address"));
1233
1234        let invalid_checksum = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAeD";
1235        let result = Address::evm_checked(invalid_checksum);
1236        assert!(result.is_err());
1237        let err = result.unwrap_err();
1238        assert!(err.to_string().contains("checksum"));
1239    }
1240
1241    #[test]
1242    fn test_overflow_protection_in_validation() {
1243        let long_string = "a".repeat(1000);
1244        let result = Address::evm_checked(&long_string);
1245        assert!(result.is_err());
1246
1247        let result = Address::substrate_checked(&long_string);
1248        assert!(result.is_err());
1249    }
1250}