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    /// Get the chain type
127    pub fn chain_type(&self) -> ChainType {
128        match self {
129            // Pure Substrate chains
130            Chain::Polkadot
131            | Chain::Kusama
132            | Chain::Acala
133            | Chain::Phala
134            | Chain::Bifrost
135            | Chain::Westend
136            | Chain::Paseo => ChainType::Substrate,
137
138            // Pure EVM chains
139            Chain::Ethereum
140            | Chain::BinanceSmartChain
141            | Chain::Polygon
142            | Chain::Avalanche
143            | Chain::Arbitrum
144            | Chain::Optimism
145            | Chain::ZkSync
146            | Chain::Base => ChainType::Evm,
147
148            // Hybrid chains (Substrate + EVM)
149            Chain::Moonbeam | Chain::Astar => ChainType::Hybrid,
150        }
151    }
152
153    /// Get the chain name
154    pub fn name(&self) -> &str {
155        match self {
156            // Substrate
157            Chain::Polkadot => "Polkadot",
158            Chain::Kusama => "Kusama",
159            Chain::Acala => "Acala",
160            Chain::Phala => "Phala",
161            Chain::Bifrost => "Bifrost",
162            Chain::Westend => "Westend",
163            Chain::Paseo => "Paseo",
164
165            // EVM L1
166            Chain::Ethereum => "Ethereum",
167            Chain::BinanceSmartChain => "Binance Smart Chain",
168            Chain::Polygon => "Polygon",
169            Chain::Avalanche => "Avalanche",
170
171            // EVM L2
172            Chain::Arbitrum => "Arbitrum",
173            Chain::Optimism => "Optimism",
174            Chain::ZkSync => "zkSync",
175            Chain::Base => "Base",
176
177            // Hybrid
178            Chain::Moonbeam => "Moonbeam",
179            Chain::Astar => "Astar",
180        }
181    }
182
183    /// Get default RPC endpoint for the chain
184    pub fn default_endpoint(&self) -> &str {
185        match self {
186            // Substrate
187            Chain::Polkadot => "wss://polkadot.api.onfinality.io/public-ws",
188            Chain::Kusama => "wss://kusama.api.onfinality.io/public-ws",
189            Chain::Acala => "wss://acala.api.onfinality.io/public-ws",
190            Chain::Phala => "wss://phala.api.onfinality.io/public-ws",
191            Chain::Bifrost => "wss://bifrost-polkadot.api.onfinality.io/public-ws",
192            Chain::Westend => "wss://westend-rpc.polkadot.io",
193            Chain::Paseo => "wss://paseo.rpc.amforc.com",
194
195            // EVM L1
196            Chain::Ethereum => "https://eth.llamarpc.com",
197            Chain::BinanceSmartChain => "https://bsc.publicnode.com",
198            Chain::Polygon => "https://polygon-rpc.com",
199            Chain::Avalanche => "https://api.avax.network/ext/bc/C/rpc",
200
201            // EVM L2
202            Chain::Arbitrum => "https://arb1.arbitrum.io/rpc",
203            Chain::Optimism => "https://mainnet.optimism.io",
204            Chain::ZkSync => "https://mainnet.era.zksync.io",
205            Chain::Base => "https://mainnet.base.org",
206
207            // Hybrid
208            Chain::Moonbeam => "wss://moonbeam.api.onfinality.io/public-ws",
209            Chain::Astar => "wss://astar.api.onfinality.io/public-ws",
210        }
211    }
212
213    /// Get multiple RPC endpoints for reliability and failover
214    pub fn rpc_endpoints(&self) -> Vec<&str> {
215        match self {
216            // Substrate
217            Chain::Polkadot => vec![
218                "wss://polkadot.api.onfinality.io/public-ws",
219                "wss://rpc.ibp.network/polkadot",
220                "wss://polkadot.dotters.network",
221            ],
222            Chain::Kusama => vec![
223                "wss://kusama.api.onfinality.io/public-ws",
224                "wss://rpc.ibp.network/kusama",
225                "wss://kusama.dotters.network",
226            ],
227            Chain::Westend => vec![
228                "wss://westend-rpc.polkadot.io",
229                "wss://rpc.ibp.network/westend",
230                "wss://westend.dotters.network",
231            ],
232            // For other chains, return the single default endpoint
233            _ => vec![self.default_endpoint()],
234        }
235    }
236
237    /// Check if chain is a Layer 2 solution
238    pub fn is_layer2(&self) -> bool {
239        matches!(
240            self,
241            Chain::Arbitrum | Chain::Optimism | Chain::ZkSync | Chain::Base
242        )
243    }
244
245    /// Check if chain supports smart contracts
246    pub fn supports_smart_contracts(&self) -> bool {
247        match self.chain_type() {
248            ChainType::Evm => true,
249            ChainType::Hybrid => true,
250            ChainType::Substrate => matches!(
251                self,
252                Chain::Acala | Chain::Phala | Chain::Moonbeam | Chain::Astar
253            ),
254        }
255    }
256
257    /// Check if this is a testnet
258    pub fn is_testnet(&self) -> bool {
259        matches!(self, Chain::Westend | Chain::Paseo)
260    }
261
262    /// Parse chain from string (case-insensitive)
263    pub fn from_str_case_insensitive(s: &str) -> Option<Self> {
264        match s.to_lowercase().as_str() {
265            // Substrate
266            "polkadot" => Some(Chain::Polkadot),
267            "kusama" => Some(Chain::Kusama),
268            "acala" => Some(Chain::Acala),
269            "phala" => Some(Chain::Phala),
270            "bifrost" => Some(Chain::Bifrost),
271            "westend" => Some(Chain::Westend),
272            "paseo" => Some(Chain::Paseo),
273
274            // EVM L1
275            "ethereum" | "eth" => Some(Chain::Ethereum),
276            "binance" | "bsc" | "binancesmartchain" => Some(Chain::BinanceSmartChain),
277            "polygon" | "matic" => Some(Chain::Polygon),
278            "avalanche" | "avax" => Some(Chain::Avalanche),
279
280            // EVM L2
281            "arbitrum" | "arb" => Some(Chain::Arbitrum),
282            "optimism" | "op" => Some(Chain::Optimism),
283            "zksync" => Some(Chain::ZkSync),
284            "base" => Some(Chain::Base),
285
286            // Hybrid
287            "moonbeam" => Some(Chain::Moonbeam),
288            "astar" => Some(Chain::Astar),
289
290            _ => None,
291        }
292    }
293
294    /// Check if an endpoint URL is for Substrate (WebSocket-based)
295    pub fn is_substrate_endpoint(endpoint: &str) -> bool {
296        endpoint.starts_with("ws://") || endpoint.starts_with("wss://")
297    }
298
299    /// Check if an endpoint URL is for EVM (HTTP-based)
300    pub fn is_evm_endpoint(endpoint: &str) -> bool {
301        endpoint.starts_with("http://") || endpoint.starts_with("https://")
302    }
303
304    /// Get the chain ID for EVM-compatible chains
305    ///
306    /// Returns None for pure Substrate chains that don't have a chain ID concept.
307    /// For EVM and Hybrid chains, returns the standard EIP-155 chain ID.
308    pub fn chain_id(&self) -> Option<u64> {
309        match self {
310            // Pure Substrate chains don't have chain IDs
311            Chain::Polkadot
312            | Chain::Kusama
313            | Chain::Acala
314            | Chain::Phala
315            | Chain::Bifrost
316            | Chain::Westend
317            | Chain::Paseo => None,
318
319            // EVM L1 chains
320            Chain::Ethereum => Some(1),
321            Chain::BinanceSmartChain => Some(56),
322            Chain::Polygon => Some(137),
323            Chain::Avalanche => Some(43114),
324
325            // EVM L2 chains
326            Chain::Arbitrum => Some(42161),
327            Chain::Optimism => Some(10),
328            Chain::ZkSync => Some(324),
329            Chain::Base => Some(8453),
330
331            // Hybrid chains (EVM side)
332            Chain::Moonbeam => Some(1284),
333            Chain::Astar => Some(592),
334        }
335    }
336
337    /// Validate that a given chain ID matches this chain
338    ///
339    /// Returns an error if the chain ID doesn't match the expected value.
340    /// For Substrate-only chains, returns an error indicating chain ID is not applicable.
341    pub fn validate_chain_id(&self, chain_id: u64) -> Result<(), ValidationError> {
342        match self.chain_id() {
343            None => Err(ValidationError::ChainIdNotFound(self.name().to_string())),
344            Some(expected) => {
345                if expected == chain_id {
346                    Ok(())
347                } else {
348                    Err(ValidationError::InvalidChainId {
349                        chain: self.name().to_string(),
350                        expected,
351                        actual: chain_id,
352                    })
353                }
354            }
355        }
356    }
357}
358
359/// Validates an EVM address format (0x followed by 40 hex characters)
360fn is_valid_evm_format(addr: &str) -> bool {
361    if !addr.starts_with("0x") {
362        return false;
363    }
364
365    let hex_part = &addr[2..];
366    hex_part.len() == 40 && hex_part.chars().all(|c| c.is_ascii_hexdigit())
367}
368
369/// Computes EIP-55 checksum for an EVM address
370///
371/// EIP-55 uses keccak256 hash of the lowercase address to determine which
372/// characters should be uppercase in the checksummed version.
373fn to_checksum_address(addr: &str) -> String {
374    // Remove 0x prefix and convert to lowercase
375    let addr_lower = addr.trim_start_matches("0x").to_lowercase();
376
377    // Hash the lowercase address
378    let mut hasher = Keccak256::new();
379    hasher.update(addr_lower.as_bytes());
380    let hash = hasher.finalize();
381
382    // Build checksummed address
383    let mut result = String::from("0x");
384    for (i, ch) in addr_lower.chars().enumerate() {
385        if ch.is_ascii_digit() {
386            result.push(ch);
387        } else {
388            // If the i-th byte of the hash is >= 8, uppercase the character
389            let hash_byte = hash[i / 2];
390            let nibble = if i % 2 == 0 {
391                hash_byte >> 4
392            } else {
393                hash_byte & 0x0f
394            };
395
396            if nibble >= 8 {
397                result.push(ch.to_ascii_uppercase());
398            } else {
399                result.push(ch);
400            }
401        }
402    }
403
404    result
405}
406
407/// Validates EIP-55 checksum for an EVM address
408///
409/// If the address is all lowercase or all uppercase, it's considered valid
410/// (no checksum). Otherwise, it must match the EIP-55 checksum.
411fn validate_eip55_checksum(addr: &str) -> bool {
412    let hex_part = &addr[2..];
413
414    // If all lowercase or all uppercase, skip checksum validation
415    // (these are valid but non-checksummed addresses)
416    let all_lower = hex_part.chars().all(|c| !c.is_ascii_uppercase());
417    let all_upper = hex_part.chars().all(|c| !c.is_ascii_lowercase());
418
419    if all_lower || all_upper {
420        return true;
421    }
422
423    // Validate checksum
424    let checksummed = to_checksum_address(addr);
425    addr == checksummed
426}
427
428/// Validates a Substrate SS58 address format and checksum
429///
430/// SS58 is a modified Base58 encoding that includes:
431/// - A network identifier prefix
432/// - The account ID
433/// - A Blake2b checksum
434fn validate_ss58_address(addr: &str) -> bool {
435    // Decode the base58 string
436    let decoded = match bs58::decode(addr).into_vec() {
437        Ok(d) => d,
438        Err(_) => return false,
439    };
440
441    // SS58 addresses must be at least 3 bytes (1 prefix + 2 checksum)
442    if decoded.len() < 3 {
443        return false;
444    }
445
446    // Extract checksum (last 2 bytes)
447    let checksum_len =
448        if decoded.len() == 3 || decoded.len() == 4 || decoded.len() == 6 || decoded.len() == 10 {
449            1
450        } else {
451            2
452        };
453
454    let body_len = decoded.len() - checksum_len;
455    let body = &decoded[..body_len];
456    let checksum = &decoded[body_len..];
457
458    // Compute expected checksum using Blake2b
459    let mut hasher = Blake2b512::new();
460    hasher.update(b"SS58PRE");
461    hasher.update(body);
462    let hash = hasher.finalize();
463
464    // Compare checksums
465    &hash[..checksum_len] == checksum
466}
467
468/// Generic address type for different chains
469#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
470pub enum Address {
471    /// Substrate SS58 address
472    Substrate(String),
473    /// EVM hex address (0x...) - validated with EIP-55 checksum
474    Evm(String),
475}
476
477impl Address {
478    /// Create a Substrate address with validation
479    ///
480    /// This function validates:
481    /// - SS58 base58 encoding
482    /// - Blake2b checksum
483    ///
484    /// # Errors
485    ///
486    /// Returns `Err` if the address format is invalid or checksum validation fails.
487    ///
488    /// # Example
489    ///
490    /// ```
491    /// use apex_sdk_types::Address;
492    ///
493    /// // Valid SS58 address (Polkadot)
494    /// let addr = Address::substrate_checked("15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5").unwrap();
495    ///
496    /// // Invalid address
497    /// let result = Address::substrate_checked("invalid");
498    /// assert!(result.is_err());
499    /// ```
500    pub fn substrate_checked(addr: impl Into<String>) -> Result<Self, ValidationError> {
501        let addr_str = addr.into();
502
503        // Validate SS58 format and checksum
504        if !validate_ss58_address(&addr_str) {
505            return Err(ValidationError::InvalidSubstrateAddress(addr_str));
506        }
507
508        Ok(Address::Substrate(addr_str))
509    }
510
511    /// Create a Substrate address without validation (legacy method)
512    ///
513    /// **Warning**: This method does not perform SS58 checksum validation.
514    /// Use `substrate_checked()` instead for new code.
515    ///
516    /// This method is provided for backward compatibility and cases where
517    /// validation is not required (e.g., trusted input sources).
518    pub fn substrate(addr: impl Into<String>) -> Self {
519        Address::Substrate(addr.into())
520    }
521
522    /// Create an EVM address with validation
523    ///
524    /// This function validates:
525    /// - Address format (0x followed by 40 hex characters)
526    /// - EIP-55 checksum (if the address has mixed case)
527    ///
528    /// # Errors
529    ///
530    /// Returns `Err` if the address format is invalid or checksum validation fails.
531    ///
532    /// # Example
533    ///
534    /// ```
535    /// use apex_sdk_types::Address;
536    ///
537    /// // Valid checksummed address
538    /// let addr = Address::evm_checked("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed").unwrap();
539    ///
540    /// // Valid lowercase address (no checksum)
541    /// let addr = Address::evm_checked("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed").unwrap();
542    ///
543    /// // Invalid checksum
544    /// let result = Address::evm_checked("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAeD");
545    /// assert!(result.is_err());
546    /// ```
547    pub fn evm_checked(addr: impl Into<String>) -> Result<Self, ValidationError> {
548        let addr_str = addr.into();
549
550        // Validate format
551        if !is_valid_evm_format(&addr_str) {
552            return Err(ValidationError::InvalidEvmAddress(addr_str));
553        }
554
555        // Validate EIP-55 checksum
556        if !validate_eip55_checksum(&addr_str) {
557            return Err(ValidationError::InvalidChecksum(addr_str));
558        }
559
560        Ok(Address::Evm(addr_str))
561    }
562
563    /// Create an EVM address without validation (legacy method)
564    ///
565    /// **Warning**: This method does not perform EIP-55 checksum validation.
566    /// Use `evm_checked()` instead for new code.
567    ///
568    /// This method is provided for backward compatibility and cases where
569    /// validation is not required (e.g., trusted input sources).
570    pub fn evm(addr: impl Into<String>) -> Self {
571        Address::Evm(addr.into())
572    }
573
574    /// Convert EVM address to checksummed format
575    ///
576    /// For EVM addresses, returns the EIP-55 checksummed version.
577    /// For Substrate addresses, returns the address unchanged.
578    pub fn to_checksum(&self) -> String {
579        match self {
580            Address::Evm(addr) => to_checksum_address(addr),
581            Address::Substrate(addr) => addr.clone(),
582        }
583    }
584
585    /// Get the address as a string
586    pub fn as_str(&self) -> &str {
587        match self {
588            Address::Substrate(s) | Address::Evm(s) => s,
589        }
590    }
591
592    /// Validate the address format and checksum
593    ///
594    /// For EVM addresses, validates EIP-55 checksum.
595    /// For Substrate addresses, always returns Ok (validation not implemented).
596    pub fn validate(&self) -> Result<(), ValidationError> {
597        match self {
598            Address::Evm(addr) => {
599                if !is_valid_evm_format(addr) {
600                    return Err(ValidationError::InvalidEvmAddress(addr.clone()));
601                }
602                if !validate_eip55_checksum(addr) {
603                    return Err(ValidationError::InvalidChecksum(addr.clone()));
604                }
605                Ok(())
606            }
607            Address::Substrate(_) => Ok(()), // TODO: Add SS58 validation
608        }
609    }
610}
611
612/// Transaction status
613#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
614pub enum TransactionStatus {
615    /// Transaction is pending.
616    ///
617    /// The transaction has been created but has not yet been broadcasted to the network.
618    /// This status typically indicates that the transaction is awaiting submission or signing.
619    Pending,
620    /// Transaction is in memory pool (mempool).
621    ///
622    /// The transaction has been broadcasted to the network and is waiting to be included in a block.
623    /// This status indicates that the transaction is known to the network but not yet confirmed.
624    InMempool,
625    /// Transaction is confirmed
626    Confirmed {
627        /// Block hash
628        block_hash: String,
629        /// Block number where transaction was included
630        block_number: Option<u64>,
631    },
632    /// Transaction is finalized (for Substrate chains)
633    Finalized {
634        /// Block hash
635        block_hash: String,
636        /// Block number
637        block_number: u64,
638    },
639    /// Transaction failed
640    Failed {
641        /// Error message
642        error: String,
643    },
644    /// Transaction status unknown
645    Unknown,
646}
647
648/// Represents a blockchain event emitted by a smart contract or runtime.
649///
650/// The `Event` struct captures details about an event, including its name, associated data,
651/// the block and transaction in which it occurred, and its index within the block.
652///
653/// # Fields
654/// - `name`: The name of the event (e.g., `"Transfer"`, `"Approval"`).
655/// - `data`: The event payload as a JSON value. This typically contains event parameters.
656/// - `block_number`: The block number in which the event was emitted, if available.
657/// - `tx_hash`: The transaction hash associated with the event, if available.
658/// - `index`: The index of the event within the block, if available.
659///
660/// # Example
661/// ```
662/// use apex_sdk_types::Event;
663/// use serde_json::json;
664///
665/// let event = Event {
666///     name: "Transfer".to_string(),
667///     data: json!({
668///         "from": "0x123...",
669///         "to": "0x456...",
670///         "value": 1000
671///     }),
672///     block_number: Some(123456),
673///     tx_hash: Some("0xabc...".to_string()),
674///     index: Some(0),
675/// };
676/// assert_eq!(event.name, "Transfer");
677/// ```
678#[derive(Debug, Clone, Serialize, Deserialize)]
679pub struct Event {
680    /// The name of the event (e.g., "Transfer", "Approval").
681    pub name: String,
682    /// The event payload as a JSON value, typically containing event parameters.
683    pub data: serde_json::Value,
684    /// The block number in which the event was emitted, if available.
685    pub block_number: Option<u64>,
686    /// The transaction hash associated with the event, if available.
687    pub tx_hash: Option<String>,
688    /// The index of the event within the block, if available.
689    pub index: Option<u32>,
690}
691
692/// Filter criteria for subscribing to blockchain events.
693///
694/// This struct allows you to specify which events to receive by name, contract address,
695/// and block range. All fields are optional; if a field is `None`, it will not be used
696/// as a filter criterion.
697#[derive(Debug, Clone, Serialize, Deserialize)]
698pub struct EventFilter {
699    /// List of event names to filter for.
700    ///
701    /// If specified, only events with names matching one of the strings in this list
702    /// will be included. If `None`, all event names are included.
703    pub event_names: Option<Vec<String>>,
704    /// List of contract addresses to filter for.
705    ///
706    /// If specified, only events emitted by contracts with addresses in this list
707    /// will be included. If `None`, events from all addresses are included.
708    pub addresses: Option<Vec<Address>>,
709    /// The starting block number (inclusive) for filtering events.
710    ///
711    /// If specified, only events from blocks with number greater than or equal to this
712    /// value will be included. If `None`, events from all blocks are included.
713    pub from_block: Option<u64>,
714    /// The ending block number (inclusive) for filtering events.
715    ///
716    /// If specified, only events from blocks with number less than or equal to this
717    /// value will be included. If `None`, events up to the latest block are included.
718    pub to_block: Option<u64>,
719}
720
721/// Cross-chain transaction info
722#[derive(Debug, Clone, Serialize, Deserialize)]
723pub struct CrossChainTransaction {
724    /// Transaction ID
725    pub id: String,
726    /// Source chain
727    pub source_chain: Chain,
728    /// Destination chain
729    pub destination_chain: Chain,
730    /// Source transaction hash
731    pub source_tx_hash: Option<String>,
732    /// Destination transaction hash
733    pub destination_tx_hash: Option<String>,
734    /// Transaction status
735    pub status: TransactionStatus,
736    /// Timestamp
737    pub timestamp: u64,
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743
744    #[test]
745    fn test_chain_type() {
746        assert_eq!(Chain::Polkadot.chain_type(), ChainType::Substrate);
747        assert_eq!(Chain::Ethereum.chain_type(), ChainType::Evm);
748        assert_eq!(Chain::Moonbeam.chain_type(), ChainType::Hybrid);
749    }
750
751    #[test]
752    fn test_address_creation() {
753        let sub_addr = Address::substrate("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY");
754        assert!(matches!(sub_addr, Address::Substrate(_)));
755
756        let evm_addr = Address::evm("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7");
757        assert!(matches!(evm_addr, Address::Evm(_)));
758    }
759
760    #[test]
761    fn test_chain_id() {
762        // EVM chains should have chain IDs
763        assert_eq!(Chain::Ethereum.chain_id(), Some(1));
764        assert_eq!(Chain::BinanceSmartChain.chain_id(), Some(56));
765        assert_eq!(Chain::Polygon.chain_id(), Some(137));
766        assert_eq!(Chain::Avalanche.chain_id(), Some(43114));
767        assert_eq!(Chain::Arbitrum.chain_id(), Some(42161));
768        assert_eq!(Chain::Optimism.chain_id(), Some(10));
769        assert_eq!(Chain::ZkSync.chain_id(), Some(324));
770        assert_eq!(Chain::Base.chain_id(), Some(8453));
771
772        // Hybrid chains should have chain IDs (EVM side)
773        assert_eq!(Chain::Moonbeam.chain_id(), Some(1284));
774        assert_eq!(Chain::Astar.chain_id(), Some(592));
775
776        // Substrate chains should not have chain IDs
777        assert_eq!(Chain::Polkadot.chain_id(), None);
778        assert_eq!(Chain::Kusama.chain_id(), None);
779        assert_eq!(Chain::Westend.chain_id(), None);
780        assert_eq!(Chain::Paseo.chain_id(), None);
781    }
782
783    #[test]
784    fn test_chain_id_validation() {
785        // Valid chain IDs
786        assert!(Chain::Ethereum.validate_chain_id(1).is_ok());
787        assert!(Chain::BinanceSmartChain.validate_chain_id(56).is_ok());
788        assert!(Chain::Polygon.validate_chain_id(137).is_ok());
789
790        // Invalid chain IDs
791        assert!(Chain::Ethereum.validate_chain_id(56).is_err());
792        assert!(Chain::Polygon.validate_chain_id(1).is_err());
793
794        // Substrate chains should return error
795        assert!(Chain::Polkadot.validate_chain_id(1).is_err());
796    }
797
798    #[test]
799    fn test_eip55_valid_checksummed_addresses() {
800        // Test vectors from EIP-55
801        let valid_addresses = vec![
802            "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
803            "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
804            "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
805            "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb",
806        ];
807
808        for addr in valid_addresses {
809            let result = Address::evm_checked(addr);
810            assert!(
811                result.is_ok(),
812                "Address {} should be valid, got error: {:?}",
813                addr,
814                result.err()
815            );
816        }
817    }
818
819    #[test]
820    fn test_eip55_lowercase_addresses() {
821        // All lowercase addresses are valid (no checksum)
822        let lowercase_addr = "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed";
823        assert!(Address::evm_checked(lowercase_addr).is_ok());
824
825        let lowercase_addr2 = "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359";
826        assert!(Address::evm_checked(lowercase_addr2).is_ok());
827    }
828
829    #[test]
830    fn test_eip55_uppercase_addresses() {
831        // All uppercase addresses are valid (no checksum)
832        let uppercase_addr = "0x5AAEB6053F3E94C9B9A09F33669435E7EF1BEAED";
833        assert!(Address::evm_checked(uppercase_addr).is_ok());
834    }
835
836    #[test]
837    fn test_eip55_invalid_checksum() {
838        // Invalid checksum (last character changed from 'd' to 'D')
839        let invalid_addr = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAeD";
840        let result = Address::evm_checked(invalid_addr);
841        assert!(result.is_err());
842        assert!(matches!(
843            result.unwrap_err(),
844            ValidationError::InvalidChecksum(_)
845        ));
846    }
847
848    #[test]
849    fn test_eip55_invalid_format() {
850        // Missing 0x prefix
851        let no_prefix = "5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed";
852        let result = Address::evm_checked(no_prefix);
853        assert!(result.is_err());
854        assert!(matches!(
855            result.unwrap_err(),
856            ValidationError::InvalidEvmAddress(_)
857        ));
858
859        // Too short
860        let too_short = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeA";
861        let result = Address::evm_checked(too_short);
862        assert!(result.is_err());
863
864        // Too long
865        let too_long = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAedAA";
866        let result = Address::evm_checked(too_long);
867        assert!(result.is_err());
868
869        // Invalid hex characters
870        let invalid_hex = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAeG";
871        let result = Address::evm_checked(invalid_hex);
872        assert!(result.is_err());
873    }
874
875    #[test]
876    fn test_to_checksum_address() {
877        let lowercase = "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed";
878        let checksummed = to_checksum_address(lowercase);
879        assert_eq!(checksummed, "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed");
880
881        let lowercase2 = "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359";
882        let checksummed2 = to_checksum_address(lowercase2);
883        assert_eq!(checksummed2, "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359");
884    }
885
886    #[test]
887    fn test_address_to_checksum_method() {
888        let addr = Address::evm("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed");
889        assert_eq!(
890            addr.to_checksum(),
891            "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"
892        );
893
894        // Substrate addresses should be unchanged
895        let sub_addr = Address::substrate("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY");
896        assert_eq!(
897            sub_addr.to_checksum(),
898            "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
899        );
900    }
901
902    #[test]
903    fn test_address_validate() {
904        // Valid checksummed EVM address
905        let addr = Address::evm("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed");
906        assert!(addr.validate().is_ok());
907
908        // Valid lowercase EVM address
909        let addr = Address::evm("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed");
910        assert!(addr.validate().is_ok());
911
912        // Invalid checksum
913        let addr = Address::evm("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAeD");
914        assert!(addr.validate().is_err());
915
916        // Invalid format
917        let addr = Address::evm("invalid");
918        assert!(addr.validate().is_err());
919
920        // Substrate addresses always pass (for now)
921        let addr = Address::substrate("anything");
922        assert!(addr.validate().is_ok());
923    }
924
925    #[test]
926    fn test_is_testnet() {
927        assert!(Chain::Westend.is_testnet());
928        assert!(Chain::Paseo.is_testnet());
929        assert!(!Chain::Polkadot.is_testnet());
930        assert!(!Chain::Ethereum.is_testnet());
931    }
932
933    #[test]
934    fn test_substrate_ss58_validation_valid_addresses() {
935        // Valid SS58 addresses (Polkadot format)
936        let valid_addresses = vec![
937            "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
938            "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
939            "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM",
940        ];
941
942        for addr in valid_addresses {
943            let result = Address::substrate_checked(addr);
944            assert!(
945                result.is_ok(),
946                "Address {} should be valid, got error: {:?}",
947                addr,
948                result.err()
949            );
950        }
951    }
952
953    #[test]
954    fn test_substrate_ss58_validation_invalid_addresses() {
955        // Invalid SS58 addresses
956        let invalid_addresses = vec![
957            "invalid",                                             // Not base58
958            "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQ",     // Too short
959            "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY123", // Too long
960            "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQX",    // Invalid checksum
961        ];
962
963        for addr in invalid_addresses {
964            let result = Address::substrate_checked(addr);
965            assert!(
966                result.is_err(),
967                "Address {} should be invalid but was accepted",
968                addr
969            );
970        }
971    }
972
973    #[test]
974    fn test_substrate_ss58_validation_error_types() {
975        // Test invalid format
976        let invalid_format = "not-base58!@#";
977        let result = Address::substrate_checked(invalid_format);
978        assert!(result.is_err());
979        match result.unwrap_err() {
980            ValidationError::InvalidSubstrateAddress(_) => (),
981            _ => panic!("Expected InvalidSubstrateAddress error"),
982        }
983
984        // Test invalid checksum (valid base58 but wrong checksum)
985        let invalid_checksum = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQX";
986        let result = Address::substrate_checked(invalid_checksum);
987        assert!(result.is_err());
988        // The error might be InvalidSubstrateAddress since the checksum validation fails
989    }
990
991    #[test]
992    fn test_validation_error_messages() {
993        // Test EVM validation error
994        let invalid_evm = "0xinvalid";
995        let result = Address::evm_checked(invalid_evm);
996        assert!(result.is_err());
997        let err = result.unwrap_err();
998        assert!(err.to_string().contains("Invalid EVM address"));
999
1000        // Test checksum error
1001        let invalid_checksum = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAeD";
1002        let result = Address::evm_checked(invalid_checksum);
1003        assert!(result.is_err());
1004        let err = result.unwrap_err();
1005        assert!(err.to_string().contains("checksum"));
1006    }
1007
1008    #[test]
1009    fn test_overflow_protection_in_validation() {
1010        // Test that validation handles edge cases without panicking
1011        let long_string = "a".repeat(1000);
1012        let result = Address::evm_checked(&long_string);
1013        assert!(result.is_err());
1014
1015        let result = Address::substrate_checked(&long_string);
1016        assert!(result.is_err());
1017    }
1018}