rustywallet_import/
detect.rs

1//! Format detection and unified import.
2
3use crate::error::{ImportError, Result};
4use crate::types::{ImportFormat, ImportResult};
5use crate::{import_wif, import_hex, import_mini_key, import_mnemonic, MnemonicImport};
6use rustywallet_hd::Network as HdNetwork;
7use rustywallet_keys::prelude::Network as KeysNetwork;
8
9/// Convert keys Network to hd Network
10fn convert_network(network: KeysNetwork) -> HdNetwork {
11    match network {
12        KeysNetwork::Mainnet => HdNetwork::Mainnet,
13        KeysNetwork::Testnet => HdNetwork::Testnet,
14    }
15}
16
17/// Detect the format of an input string.
18///
19/// # Example
20///
21/// ```rust
22/// use rustywallet_import::{detect_format, ImportFormat};
23///
24/// assert_eq!(detect_format("5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ"), Some(ImportFormat::Wif));
25/// assert_eq!(detect_format("0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d"), Some(ImportFormat::Hex));
26/// ```
27pub fn detect_format(input: &str) -> Option<ImportFormat> {
28    let input = input.trim();
29    
30    // BIP38: starts with 6P, 58 characters
31    if input.starts_with("6P") && input.len() == 58 {
32        return Some(ImportFormat::Bip38);
33    }
34    
35    // WIF: 51-52 characters, starts with 5, K, L (mainnet) or 9, c (testnet)
36    if (input.len() == 51 || input.len() == 52) && 
37       (input.starts_with('5') || input.starts_with('K') || 
38        input.starts_with('L') || input.starts_with('9') || 
39        input.starts_with('c')) {
40        return Some(ImportFormat::Wif);
41    }
42    
43    // Hex: 64 hex characters (with optional 0x prefix)
44    let hex_input = input.strip_prefix("0x").or_else(|| input.strip_prefix("0X")).unwrap_or(input);
45    if hex_input.len() == 64 && hex_input.chars().all(|c| c.is_ascii_hexdigit()) {
46        return Some(ImportFormat::Hex);
47    }
48    
49    // Mini key: starts with S, 22 or 30 characters
50    if input.starts_with('S') && (input.len() == 22 || input.len() == 30) {
51        return Some(ImportFormat::MiniKey);
52    }
53    
54    // Mnemonic: 12, 15, 18, 21, or 24 words
55    let words: Vec<&str> = input.split_whitespace().collect();
56    if [12, 15, 18, 21, 24].contains(&words.len()) {
57        return Some(ImportFormat::Mnemonic);
58    }
59    
60    None
61}
62
63/// Import a private key from any supported format.
64///
65/// Automatically detects the format and imports accordingly.
66/// For mnemonic imports, uses default BIP44 path (m/44'/0'/0'/0/0).
67///
68/// # Example
69///
70/// ```rust
71/// use rustywallet_import::import_any;
72///
73/// // WIF
74/// let result = import_any("5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ").unwrap();
75/// println!("Format: {}", result.format);
76///
77/// // Hex
78/// let result = import_any("0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d").unwrap();
79/// println!("Format: {}", result.format);
80/// ```
81pub fn import_any(input: &str) -> Result<ImportResult> {
82    let input = input.trim();
83    
84    let format = detect_format(input)
85        .ok_or_else(|| ImportError::InvalidFormat("Could not detect format".to_string()))?;
86    
87    match format {
88        ImportFormat::Wif => {
89            let (key, network, compressed) = import_wif(input)?;
90            Ok(ImportResult::new(key, ImportFormat::Wif)
91                .with_network(convert_network(network))
92                .with_compressed(compressed))
93        }
94        ImportFormat::Hex => {
95            let key = import_hex(input)?;
96            Ok(ImportResult::new(key, ImportFormat::Hex)
97                .with_compressed(true))
98        }
99        ImportFormat::MiniKey => {
100            let key = import_mini_key(input)?;
101            Ok(ImportResult::new(key, ImportFormat::MiniKey)
102                .with_compressed(false)) // Mini keys traditionally use uncompressed
103        }
104        ImportFormat::Mnemonic => {
105            let config = MnemonicImport::new(input);
106            import_mnemonic(config)
107        }
108        ImportFormat::Bip38 => {
109            Err(ImportError::InvalidFormat(
110                "BIP38 requires password - use import_bip38() directly".to_string()
111            ))
112        }
113        ImportFormat::ElectrumSeed => {
114            Err(ImportError::UnsupportedFormat(
115                "Electrum seed format not yet supported".to_string()
116            ))
117        }
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    
125    #[test]
126    fn test_detect_wif_uncompressed() {
127        let wif = "5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ";
128        assert_eq!(detect_format(wif), Some(ImportFormat::Wif));
129    }
130    
131    #[test]
132    fn test_detect_wif_compressed() {
133        let wif = "KwdMAjGmerYanjeui5SHS7JkmpZvVipYvB2LJGU1ZxJwYvP98617";
134        assert_eq!(detect_format(wif), Some(ImportFormat::Wif));
135    }
136    
137    #[test]
138    fn test_detect_hex() {
139        let hex = "0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d";
140        assert_eq!(detect_format(hex), Some(ImportFormat::Hex));
141    }
142    
143    #[test]
144    fn test_detect_hex_with_prefix() {
145        let hex = "0x0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d";
146        assert_eq!(detect_format(hex), Some(ImportFormat::Hex));
147    }
148    
149    #[test]
150    fn test_detect_bip38() {
151        let bip38 = "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg";
152        assert_eq!(detect_format(bip38), Some(ImportFormat::Bip38));
153    }
154    
155    #[test]
156    fn test_detect_mini_key() {
157        let mini = "S6c56bnXQiBjk9mqSYE7ykVQ7NzrRy";
158        assert_eq!(detect_format(mini), Some(ImportFormat::MiniKey));
159    }
160    
161    #[test]
162    fn test_detect_mnemonic() {
163        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
164        assert_eq!(detect_format(mnemonic), Some(ImportFormat::Mnemonic));
165    }
166    
167    #[test]
168    fn test_detect_unknown() {
169        let unknown = "not a valid format";
170        assert_eq!(detect_format(unknown), None);
171    }
172    
173    #[test]
174    fn test_import_any_wif() {
175        let wif = "5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ";
176        let result = import_any(wif).unwrap();
177        assert_eq!(result.format, ImportFormat::Wif);
178        assert!(!result.compressed);
179    }
180    
181    #[test]
182    fn test_import_any_hex() {
183        let hex = "0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d";
184        let result = import_any(hex).unwrap();
185        assert_eq!(result.format, ImportFormat::Hex);
186    }
187    
188    #[test]
189    fn test_import_any_mnemonic() {
190        let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
191        let result = import_any(mnemonic).unwrap();
192        assert_eq!(result.format, ImportFormat::Mnemonic);
193    }
194    
195    #[test]
196    fn test_import_any_bip38_requires_password() {
197        let bip38 = "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg";
198        let result = import_any(bip38);
199        assert!(matches!(result, Err(ImportError::InvalidFormat(_))));
200    }
201}