foxchain_id/
lib.rs

1//! Foxchain ID: Multi-chain blockchain address identification
2//!
3//! This crate provides functionality to identify which blockchain(s) an input
4//! string (address, public key, or private key) belongs to.
5
6mod detectors;
7mod identify;
8mod input;
9mod loaders;
10mod models;
11mod pipelines;
12mod registry;
13mod shared;
14
15pub use identify::{identify as identify_all, IdentificationCandidate, InputType};
16
17/// Identify the blockchain(s) for a given input string.
18///
19/// Returns all valid candidates sorted by confidence (highest first).
20/// This function supports ambiguous inputs that may match multiple chains.
21///
22/// # Example
23///
24/// ```rust
25/// use foxchain_id::identify;
26///
27/// let candidates = identify("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")?;
28/// for candidate in candidates {
29///     println!("Chain: {:?}, Confidence: {}, Normalized: {}",
30///              candidate.chain, candidate.confidence, candidate.normalized);
31/// }
32/// # Ok::<(), foxchain_id::Error>(())
33/// ```
34pub fn identify(input: &str) -> Result<Vec<IdentificationCandidate>, Error> {
35    identify_all(input)
36}
37
38/// Errors that can occur during identification
39#[derive(Debug, Clone)]
40pub enum Error {
41    /// Feature not yet implemented
42    NotImplemented,
43    /// Invalid input format
44    InvalidInput(String),
45}
46
47impl std::fmt::Display for Error {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        match self {
50            Error::NotImplemented => write!(f, "Feature not yet implemented"),
51            Error::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
52        }
53    }
54}
55
56impl std::error::Error for Error {}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn test_identify_evm_address() {
64        // Test with lowercase address - should be normalized
65        let input = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045";
66        let result = identify(input);
67        if let Err(e) = &result {
68            eprintln!("Error: {}", e);
69        }
70        assert!(result.is_ok());
71        let candidates = result.unwrap();
72        assert!(!candidates.is_empty());
73        // Should return multiple EVM chains
74        assert!(candidates.iter().any(|c| c.chain == "ethereum"));
75        // First candidate should have highest confidence
76        assert!(candidates[0].confidence > 0.0);
77        // Should be normalized to checksum format
78        assert_ne!(candidates[0].normalized, input);
79        assert!(candidates[0].normalized.starts_with("0x"));
80        assert_eq!(candidates[0].normalized.len(), 42);
81    }
82
83    #[test]
84    fn test_identify_evm_address_lowercase() {
85        let input = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045";
86        let result = identify(input);
87        assert!(result.is_ok());
88        let candidates = result.unwrap();
89        assert!(!candidates.is_empty());
90        // Should be normalized to checksum format (different from input)
91        assert_ne!(candidates[0].normalized, input);
92        assert!(candidates[0].normalized.starts_with("0x"));
93        assert_eq!(candidates[0].normalized.len(), 42);
94    }
95
96    #[test]
97    fn test_identify_evm_multiple_chains() {
98        // EVM addresses should return multiple chain candidates
99        let input = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e";
100        let result = identify(input);
101        assert!(result.is_ok());
102        let candidates = result.unwrap();
103        // Should have multiple EVM chains
104        assert!(candidates.len() >= 1);
105        // All should be EVM chains
106        let evm_chains = [
107            "ethereum",
108            "polygon",
109            "bsc",
110            "avalanche",
111            "arbitrum",
112            "optimism",
113            "base",
114            "fantom",
115            "celo",
116            "gnosis",
117        ];
118        assert!(candidates
119            .iter()
120            .all(|c| evm_chains.contains(&c.chain.as_str())));
121    }
122
123    #[test]
124    fn test_identify_invalid_address() {
125        let result = identify("not-an-address");
126        assert!(result.is_err());
127        // Verify error message contains the input
128        if let Err(Error::InvalidInput(msg)) = result {
129            assert!(msg.contains("not-an-address"));
130        } else {
131            panic!("Expected InvalidInput error");
132        }
133    }
134
135    #[test]
136    fn test_identify_unrecognized_format() {
137        // Test with a string that doesn't match any known format
138        // This should trigger the classifier error path (returns early)
139        let result = identify("xyz123abc");
140        assert!(result.is_err());
141        if let Err(Error::InvalidInput(msg)) = result {
142            // Classifier returns "Unable to classify input format" when no possibilities found
143            assert!(
144                msg.contains("Unable to classify input format")
145                    || msg.contains("Unable to identify address format")
146            );
147            assert!(msg.contains("xyz123abc"));
148        } else {
149            panic!("Expected InvalidInput error");
150        }
151    }
152
153    #[test]
154    fn test_identify_empty_string() {
155        // Test with empty string
156        let result = identify("");
157        assert!(result.is_err());
158        if let Err(Error::InvalidInput(msg)) = result {
159            // Classifier returns "Unable to classify input format" when no possibilities found
160            assert!(
161                msg.contains("Unable to classify input format")
162                    || msg.contains("Unable to identify address format")
163            );
164        } else {
165            panic!("Expected InvalidInput error");
166        }
167    }
168
169    #[test]
170    fn test_identify_tron() {
171        // Test Tron address identification
172        // Create a valid test Tron address
173        use base58::ToBase58;
174        use sha2::{Digest, Sha256};
175
176        let version = 0x41u8;
177        let address_bytes = vec![0u8; 20];
178        let payload = [&[version], address_bytes.as_slice()].concat();
179        let hash1 = Sha256::digest(&payload);
180        let hash2 = Sha256::digest(hash1);
181        let checksum = &hash2[..4];
182        let full_bytes = [payload, checksum.to_vec()].concat();
183        let tron_addr = full_bytes.to_base58();
184
185        let result = identify(&tron_addr);
186        // May succeed or fail depending on validation
187        if result.is_ok() {
188            let candidates = result.unwrap();
189            assert!(!candidates.is_empty());
190            assert!(candidates.iter().any(|c| c.chain == "tron"));
191        }
192    }
193
194    #[test]
195    fn test_identify_substrate() {
196        // Test Substrate address identification
197        use base58::ToBase58;
198        // Create a valid test Substrate address (prefix 0 = Polkadot)
199        let mut bytes = vec![0u8]; // Prefix
200        bytes.extend(vec![0u8; 32]); // Account ID
201        bytes.extend(vec![0u8; 2]); // Checksum
202        let substrate_addr = bytes.to_base58();
203
204        let result = identify(&substrate_addr);
205        // This may fail if the address doesn't validate, but tests integration
206        if result.is_ok() {
207            let candidates = result.unwrap();
208            // Should have Substrate chain candidates if valid
209            assert!(!candidates.is_empty());
210        }
211    }
212}