three_word_networking/
multiaddr_parser.rs

1//! Multiaddr Parser Module
2//!
3//! Parses multiaddr strings into structured components that can be deterministically
4//! mapped to three-word addresses without requiring external registry lookups.
5
6use crate::error::{ThreeWordError, Result};
7use serde::{Deserialize, Serialize};
8use std::net::{Ipv4Addr, Ipv6Addr};
9use std::str::FromStr;
10
11/// Represents different IP address types found in multiaddrs
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum IpType {
14    IPv4,
15    IPv6,
16    DNS4,
17    DNS6,
18    DNS,
19    Unix,
20    P2P,
21    Onion,
22    Onion3,
23    Garlic64,
24    Garlic32,
25    Memory,
26    CIDv1,
27    SCTP,
28    UTP,
29    /// Unknown IP type with string name
30    Unknown(String),
31}
32
33/// Represents different transport protocols found in multiaddrs
34#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
35pub enum Protocol {
36    TCP,
37    UDP,
38    DCCP,
39    SCTP,
40    UTP,
41    QUIC,
42    QuicV1,
43    WS,
44    WSS,
45    WebSocket,
46    TLS,
47    Noise,
48    Yamux,
49    MPLEX,
50    HTTP,
51    HTTPS,
52    HTTPPath,
53    P2PCircuit,
54    P2PWebSocket,
55    P2PWebSocketStar,
56    P2PStardust,
57    WebRTC,
58    WebRTCDirect,
59    WebTransport,
60    Certhash,
61    Plaintextv2,
62    /// Unknown protocol with string name and optional port requirement
63    Unknown(String, bool), // (protocol_name, has_port)
64}
65
66/// Represents a parsed multiaddr with its constituent components
67#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
68pub struct ParsedMultiaddr {
69    pub ip_type: IpType,
70    pub address: String,
71    pub protocol: Protocol,
72    pub port: u16,
73    pub additional_protocols: Vec<Protocol>,
74}
75
76impl ParsedMultiaddr {
77    /// Parse a multiaddr string into its components
78    pub fn parse(multiaddr: &str) -> Result<Self> {
79        if !multiaddr.starts_with('/') {
80            return Err(ThreeWordError::InvalidMultiaddr(
81                format!("Multiaddr must start with '/', got: {}", multiaddr)
82            ));
83        }
84
85        let parts: Vec<&str> = multiaddr.split('/').filter(|s| !s.is_empty()).collect();
86        
87        if parts.len() < 3 {
88            return Err(ThreeWordError::InvalidMultiaddr(
89                format!("Multiaddr must have at least 3 parts, got: {}", parts.len())
90            ));
91        }
92
93        // Parse IP type and address
94        let (ip_type, address) = Self::parse_ip_component(&parts[0], &parts[1])?;
95        
96        // Parse protocol and port
97        let (protocol, port) = if parts.len() > 3 {
98            Self::parse_protocol_component(&parts[2], &parts[3])?
99        } else {
100            // Handle protocols without separate port (like /ip4/127.0.0.1/quic)
101            Self::parse_protocol_component(&parts[2], "0")?
102        };
103        
104        // Parse additional protocols
105        let additional_protocols = if parts.len() > 4 {
106            Self::parse_additional_protocols(&parts[4..])?
107        } else if parts.len() == 4 && matches!(protocol, Protocol::UDP | Protocol::TCP) {
108            // If we have 4 parts and the 3rd is UDP/TCP, the 4th might be an additional protocol
109            if parts[3].parse::<u16>().is_err() {
110                Self::parse_additional_protocols(&parts[3..])?
111            } else {
112                vec![]
113            }
114        } else {
115            vec![]
116        };
117
118        Ok(ParsedMultiaddr {
119            ip_type,
120            address,
121            protocol,
122            port,
123            additional_protocols,
124        })
125    }
126
127    /// Convert parsed components back to multiaddr string
128    pub fn to_multiaddr(&self) -> String {
129        let mut result = String::new();
130        
131        // Add IP type and address
132        match self.ip_type {
133            IpType::IPv4 => result.push_str(&format!("/ip4/{}", self.address)),
134            IpType::IPv6 => result.push_str(&format!("/ip6/{}", self.address)),
135            IpType::DNS4 => result.push_str(&format!("/dns4/{}", self.address)),
136            IpType::DNS6 => result.push_str(&format!("/dns6/{}", self.address)),
137            IpType::DNS => result.push_str(&format!("/dns/{}", self.address)),
138            IpType::Unix => result.push_str(&format!("/unix/{}", self.address)),
139            IpType::P2P => result.push_str(&format!("/p2p/{}", self.address)),
140            IpType::Onion => result.push_str(&format!("/onion/{}", self.address)),
141            IpType::Onion3 => result.push_str(&format!("/onion3/{}", self.address)),
142            IpType::Garlic64 => result.push_str(&format!("/garlic64/{}", self.address)),
143            IpType::Garlic32 => result.push_str(&format!("/garlic32/{}", self.address)),
144            IpType::Memory => result.push_str(&format!("/memory/{}", self.address)),
145            IpType::CIDv1 => result.push_str(&format!("/cid/{}", self.address)),
146            IpType::SCTP => result.push_str(&format!("/sctp/{}", self.address)),
147            IpType::UTP => result.push_str(&format!("/utp/{}", self.address)),
148            IpType::Unknown(ref name) => result.push_str(&format!("/{}/{}", name, self.address)),
149        }
150        
151        // Add protocol and port
152        match self.protocol {
153            Protocol::TCP => result.push_str(&format!("/tcp/{}", self.port)),
154            Protocol::UDP => result.push_str(&format!("/udp/{}", self.port)),
155            Protocol::DCCP => result.push_str(&format!("/dccp/{}", self.port)),
156            Protocol::SCTP => result.push_str(&format!("/sctp/{}", self.port)),
157            Protocol::UTP => result.push_str(&format!("/utp/{}", self.port)),
158            Protocol::QUIC => result.push_str("/quic"),
159            Protocol::QuicV1 => result.push_str("/quic-v1"),
160            Protocol::WS => result.push_str("/ws"),
161            Protocol::WSS => result.push_str("/wss"),
162            Protocol::WebSocket => result.push_str("/websocket"),
163            Protocol::TLS => result.push_str("/tls"),
164            Protocol::Noise => result.push_str("/noise"),
165            Protocol::Yamux => result.push_str("/yamux"),
166            Protocol::MPLEX => result.push_str("/mplex"),
167            Protocol::HTTP => result.push_str("/http"),
168            Protocol::HTTPS => result.push_str("/https"),
169            Protocol::HTTPPath => result.push_str("/http-path"),
170            Protocol::P2PCircuit => result.push_str("/p2p-circuit"),
171            Protocol::P2PWebSocket => result.push_str("/p2p-websocket"),
172            Protocol::P2PWebSocketStar => result.push_str("/p2p-websocket-star"),
173            Protocol::P2PStardust => result.push_str("/p2p-stardust"),
174            Protocol::WebRTC => result.push_str("/webrtc"),
175            Protocol::WebRTCDirect => result.push_str("/webrtc-direct"),
176            Protocol::WebTransport => result.push_str("/webtransport"),
177            Protocol::Certhash => result.push_str("/certhash"),
178            Protocol::Plaintextv2 => result.push_str("/plaintextv2"),
179            Protocol::Unknown(ref name, has_port) => {
180                if has_port {
181                    result.push_str(&format!("/{}/{}", name, self.port));
182                } else {
183                    result.push_str(&format!("/{}", name));
184                }
185            },
186        }
187        
188        // Add additional protocols
189        for protocol in &self.additional_protocols {
190            match protocol {
191                Protocol::QUIC => result.push_str("/quic"),
192                Protocol::WS => result.push_str("/ws"),
193                Protocol::WSS => result.push_str("/wss"),
194                Protocol::TLS => result.push_str("/tls"),
195                Protocol::HTTP => result.push_str("/http"),
196                Protocol::HTTPS => result.push_str("/https"),
197                Protocol::P2PCircuit => result.push_str("/p2p-circuit"),
198                Protocol::WebRTC => result.push_str("/webrtc"),
199                Protocol::WebTransport => result.push_str("/webtransport"),
200                _ => {} // Skip protocols that don't appear as additional
201            }
202        }
203        
204        result
205    }
206
207    fn parse_ip_component(ip_type_str: &str, address_str: &str) -> Result<(IpType, String)> {
208        let ip_type = match ip_type_str {
209            "ip4" => {
210                // Validate IPv4 address
211                Ipv4Addr::from_str(address_str)
212                    .map_err(|e| ThreeWordError::InvalidMultiaddr(
213                        format!("Invalid IPv4 address '{}': {}", address_str, e)
214                    ))?;
215                IpType::IPv4
216            }
217            "ip6" => {
218                // Validate IPv6 address
219                Ipv6Addr::from_str(address_str)
220                    .map_err(|e| ThreeWordError::InvalidMultiaddr(
221                        format!("Invalid IPv6 address '{}': {}", address_str, e)
222                    ))?;
223                IpType::IPv6
224            }
225            "dns4" => IpType::DNS4,
226            "dns6" => IpType::DNS6,
227            "dns" => IpType::DNS,
228            "unix" => IpType::Unix,
229            "p2p" => IpType::P2P,
230            "onion" => IpType::Onion,
231            "onion3" => IpType::Onion3,
232            "garlic64" => IpType::Garlic64,
233            "garlic32" => IpType::Garlic32,
234            "memory" => IpType::Memory,
235            "cid" => IpType::CIDv1,
236            "sctp" => IpType::SCTP,
237            "utp" => IpType::UTP,
238            _ => {
239                // Handle unknown IP types gracefully
240                IpType::Unknown(ip_type_str.to_string())
241            },
242        };
243
244        Ok((ip_type, address_str.to_string()))
245    }
246
247    fn parse_protocol_component(protocol_str: &str, port_str: &str) -> Result<(Protocol, u16)> {
248        let protocol = match protocol_str {
249            "tcp" => Protocol::TCP,
250            "udp" => Protocol::UDP,
251            "dccp" => Protocol::DCCP,
252            "sctp" => Protocol::SCTP,
253            "utp" => Protocol::UTP,
254            "quic" => return Ok((Protocol::QUIC, 0)),
255            "quic-v1" => return Ok((Protocol::QuicV1, 0)),
256            "ws" => return Ok((Protocol::WS, 0)),
257            "wss" => return Ok((Protocol::WSS, 0)),
258            "websocket" => return Ok((Protocol::WebSocket, 0)),
259            "tls" => return Ok((Protocol::TLS, 0)),
260            "noise" => return Ok((Protocol::Noise, 0)),
261            "yamux" => return Ok((Protocol::Yamux, 0)),
262            "mplex" => return Ok((Protocol::MPLEX, 0)),
263            "http" => return Ok((Protocol::HTTP, 0)),
264            "https" => return Ok((Protocol::HTTPS, 0)),
265            "http-path" => return Ok((Protocol::HTTPPath, 0)),
266            "p2p-circuit" => return Ok((Protocol::P2PCircuit, 0)),
267            "p2p-websocket" => return Ok((Protocol::P2PWebSocket, 0)),
268            "p2p-websocket-star" => return Ok((Protocol::P2PWebSocketStar, 0)),
269            "p2p-stardust" => return Ok((Protocol::P2PStardust, 0)),
270            "webrtc" => return Ok((Protocol::WebRTC, 0)),
271            "webrtc-direct" => return Ok((Protocol::WebRTCDirect, 0)),
272            "webtransport" => return Ok((Protocol::WebTransport, 0)),
273            "certhash" => return Ok((Protocol::Certhash, 0)),
274            "plaintextv2" => return Ok((Protocol::Plaintextv2, 0)),
275            _ => {
276                // Handle unknown protocols gracefully - assume they need a port if port_str is provided
277                let has_port = !port_str.is_empty() && port_str != "0";
278                if has_port {
279                    let port = port_str.parse::<u16>()
280                        .map_err(|e| ThreeWordError::InvalidMultiaddr(
281                            format!("Invalid port '{}' for unknown protocol '{}': {}", port_str, protocol_str, e)
282                        ))?;
283                    return Ok((Protocol::Unknown(protocol_str.to_string(), true), port));
284                } else {
285                    return Ok((Protocol::Unknown(protocol_str.to_string(), false), 0));
286                }
287            },
288        };
289
290        let port = port_str.parse::<u16>()
291            .map_err(|e| ThreeWordError::InvalidMultiaddr(
292                format!("Invalid port '{}': {}", port_str, e)
293            ))?;
294
295        Ok((protocol, port))
296    }
297
298    fn parse_additional_protocols(parts: &[&str]) -> Result<Vec<Protocol>> {
299        let mut protocols = Vec::new();
300        
301        for part in parts {
302            let protocol = match *part {
303                "quic" => Protocol::QUIC,
304                "ws" => Protocol::WS,
305                "wss" => Protocol::WSS,
306                "tls" => Protocol::TLS,
307                "http" => Protocol::HTTP,
308                "https" => Protocol::HTTPS,
309                "p2p-circuit" => Protocol::P2PCircuit,
310                "webrtc" => Protocol::WebRTC,
311                "webtransport" => Protocol::WebTransport,
312                _ => return Err(ThreeWordError::InvalidMultiaddr(
313                    format!("Unknown additional protocol: {}", part)
314                )),
315            };
316            protocols.push(protocol);
317        }
318        
319        Ok(protocols)
320    }
321
322    /// Get a compact hash representation of the address for compression
323    pub fn address_hash(&self) -> u64 {
324        use std::collections::hash_map::DefaultHasher;
325        use std::hash::{Hash, Hasher};
326        
327        let mut hasher = DefaultHasher::new();
328        self.address.hash(&mut hasher);
329        hasher.finish()
330    }
331
332    /// Get the primary protocol (first non-transport protocol)
333    pub fn primary_protocol(&self) -> Protocol {
334        if !self.additional_protocols.is_empty() {
335            self.additional_protocols[0].clone()
336        } else {
337            self.protocol.clone()
338        }
339    }
340}
341
342impl IpType {
343    /// Convert IP type to string representation
344    pub fn to_string(&self) -> String {
345        match self {
346            IpType::IPv4 => "ipv4".to_string(),
347            IpType::IPv6 => "ipv6".to_string(),
348            IpType::DNS4 => "dns4".to_string(),
349            IpType::DNS6 => "dns6".to_string(),
350            IpType::DNS => "dns".to_string(),
351            IpType::Unix => "unix".to_string(),
352            IpType::P2P => "p2p".to_string(),
353            IpType::Onion => "onion".to_string(),
354            IpType::Onion3 => "onion3".to_string(),
355            IpType::Garlic64 => "garlic64".to_string(),
356            IpType::Garlic32 => "garlic32".to_string(),
357            IpType::Memory => "memory".to_string(),
358            IpType::CIDv1 => "cid".to_string(),
359            IpType::SCTP => "sctp".to_string(),
360            IpType::UTP => "utp".to_string(),
361            IpType::Unknown(name) => name.clone(),
362        }
363    }
364    
365    /// Get word index for this IP type (for context words)
366    pub fn word_index(&self) -> usize {
367        match self {
368            IpType::IPv4 => 0,
369            IpType::IPv6 => 1,
370            IpType::DNS4 => 2,
371            IpType::DNS6 => 3,
372            IpType::DNS => 4,
373            IpType::Unix => 5,
374            IpType::P2P => 6,
375            IpType::Onion => 7,
376            IpType::Onion3 => 8,
377            IpType::Garlic64 => 9,
378            IpType::Garlic32 => 10,
379            IpType::Memory => 11,
380            IpType::CIDv1 => 12,
381            IpType::SCTP => 13,
382            IpType::UTP => 14,
383            IpType::Unknown(ref name) => {
384                // Use hash of the unknown type name for consistent mapping
385                15 + (name.len() % 100)
386            },
387        }
388    }
389
390    /// Get IP type from word index
391    pub fn from_word_index(index: usize) -> Option<Self> {
392        match index {
393            0 => Some(IpType::IPv4),
394            1 => Some(IpType::IPv6),
395            2 => Some(IpType::DNS4),
396            3 => Some(IpType::DNS6),
397            4 => Some(IpType::DNS),
398            5 => Some(IpType::Unix),
399            6 => Some(IpType::P2P),
400            7 => Some(IpType::Onion),
401            8 => Some(IpType::Onion3),
402            9 => Some(IpType::Garlic64),
403            10 => Some(IpType::Garlic32),
404            11 => Some(IpType::Memory),
405            12 => Some(IpType::CIDv1),
406            13 => Some(IpType::SCTP),
407            14 => Some(IpType::UTP),
408            _ => None,
409        }
410    }
411}
412
413impl Protocol {
414    /// Get word index for this protocol (for quality words)
415    pub fn word_index(&self) -> usize {
416        match self {
417            Protocol::TCP => 0,
418            Protocol::UDP => 1,
419            Protocol::DCCP => 2,
420            Protocol::SCTP => 3,
421            Protocol::UTP => 4,
422            Protocol::QUIC => 5,
423            Protocol::QuicV1 => 6,
424            Protocol::WS => 7,
425            Protocol::WSS => 8,
426            Protocol::WebSocket => 9,
427            Protocol::TLS => 10,
428            Protocol::Noise => 11,
429            Protocol::Yamux => 12,
430            Protocol::MPLEX => 13,
431            Protocol::HTTP => 14,
432            Protocol::HTTPS => 15,
433            Protocol::HTTPPath => 16,
434            Protocol::P2PCircuit => 17,
435            Protocol::P2PWebSocket => 18,
436            Protocol::P2PWebSocketStar => 19,
437            Protocol::P2PStardust => 20,
438            Protocol::WebRTC => 21,
439            Protocol::WebRTCDirect => 22,
440            Protocol::WebTransport => 23,
441            Protocol::Certhash => 24,
442            Protocol::Plaintextv2 => 25,
443            Protocol::Unknown(ref name, _) => {
444                // Use hash of the unknown protocol name for consistent mapping
445                26 + (name.len() % 100)
446            },
447        }
448    }
449
450    /// Get protocol from word index
451    pub fn from_word_index(index: usize) -> Option<Self> {
452        match index {
453            0 => Some(Protocol::TCP),
454            1 => Some(Protocol::UDP),
455            2 => Some(Protocol::DCCP),
456            3 => Some(Protocol::SCTP),
457            4 => Some(Protocol::UTP),
458            5 => Some(Protocol::QUIC),
459            6 => Some(Protocol::QuicV1),
460            7 => Some(Protocol::WS),
461            8 => Some(Protocol::WSS),
462            9 => Some(Protocol::WebSocket),
463            10 => Some(Protocol::TLS),
464            11 => Some(Protocol::Noise),
465            12 => Some(Protocol::Yamux),
466            13 => Some(Protocol::MPLEX),
467            14 => Some(Protocol::HTTP),
468            15 => Some(Protocol::HTTPS),
469            16 => Some(Protocol::HTTPPath),
470            17 => Some(Protocol::P2PCircuit),
471            18 => Some(Protocol::P2PWebSocket),
472            19 => Some(Protocol::P2PWebSocketStar),
473            20 => Some(Protocol::P2PStardust),
474            21 => Some(Protocol::WebRTC),
475            22 => Some(Protocol::WebRTCDirect),
476            23 => Some(Protocol::WebTransport),
477            24 => Some(Protocol::Certhash),
478            25 => Some(Protocol::Plaintextv2),
479            _ => None,
480        }
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487
488    #[test]
489    fn test_parse_ipv4_tcp() {
490        let multiaddr = "/ip4/192.168.1.1/tcp/8080";
491        let parsed = ParsedMultiaddr::parse(multiaddr).unwrap();
492        
493        assert_eq!(parsed.ip_type, IpType::IPv4);
494        assert_eq!(parsed.address, "192.168.1.1");
495        assert_eq!(parsed.protocol, Protocol::TCP);
496        assert_eq!(parsed.port, 8080);
497        assert!(parsed.additional_protocols.is_empty());
498        
499        // Test round trip
500        assert_eq!(parsed.to_multiaddr(), multiaddr);
501    }
502
503    #[test]
504    fn test_parse_ipv6_udp_quic() {
505        let multiaddr = "/ip6/2001:db8::1/udp/9000/quic";
506        let parsed = ParsedMultiaddr::parse(multiaddr).unwrap();
507        
508        assert_eq!(parsed.ip_type, IpType::IPv6);
509        assert_eq!(parsed.address, "2001:db8::1");
510        assert_eq!(parsed.protocol, Protocol::UDP);
511        assert_eq!(parsed.port, 9000);
512        assert_eq!(parsed.additional_protocols, vec![Protocol::QUIC]);
513        
514        // Test round trip
515        assert_eq!(parsed.to_multiaddr(), multiaddr);
516    }
517
518    #[test]
519    fn test_parse_dns4_tcp() {
520        let multiaddr = "/dns4/example.com/tcp/80";
521        let parsed = ParsedMultiaddr::parse(multiaddr).unwrap();
522        
523        assert_eq!(parsed.ip_type, IpType::DNS4);
524        assert_eq!(parsed.address, "example.com");
525        assert_eq!(parsed.protocol, Protocol::TCP);
526        assert_eq!(parsed.port, 80);
527        
528        // Test round trip
529        assert_eq!(parsed.to_multiaddr(), multiaddr);
530    }
531
532    #[test]
533    fn test_parse_invalid_multiaddr() {
534        // Missing leading slash
535        assert!(ParsedMultiaddr::parse("ip4/127.0.0.1/tcp/8080").is_err());
536        
537        // Too few parts
538        assert!(ParsedMultiaddr::parse("/ip4").is_err());
539        
540        // Invalid IPv4
541        assert!(ParsedMultiaddr::parse("/ip4/invalid/tcp/8080").is_err());
542        
543        // Invalid port
544        assert!(ParsedMultiaddr::parse("/ip4/127.0.0.1/tcp/invalid").is_err());
545    }
546
547    #[test]
548    fn test_ip_type_word_indices() {
549        assert_eq!(IpType::IPv4.word_index(), 0);
550        assert_eq!(IpType::IPv6.word_index(), 1);
551        assert_eq!(IpType::DNS4.word_index(), 2);
552        
553        assert_eq!(IpType::from_word_index(0), Some(IpType::IPv4));
554        assert_eq!(IpType::from_word_index(1), Some(IpType::IPv6));
555        assert_eq!(IpType::from_word_index(999), None);
556    }
557
558    #[test]
559    fn test_protocol_word_indices() {
560        assert_eq!(Protocol::TCP.word_index(), 0);
561        assert_eq!(Protocol::UDP.word_index(), 1);
562        assert_eq!(Protocol::QUIC.word_index(), 5);
563        
564        assert_eq!(Protocol::from_word_index(0), Some(Protocol::TCP));
565        assert_eq!(Protocol::from_word_index(1), Some(Protocol::UDP));
566        assert_eq!(Protocol::from_word_index(999), None);
567    }
568    
569    #[test]
570    fn test_unknown_protocol_parsing() {
571        let multiaddr = "/ip4/192.168.1.1/future-protocol/1234";
572        let parsed = ParsedMultiaddr::parse(multiaddr).unwrap();
573        
574        assert_eq!(parsed.ip_type, IpType::IPv4);
575        assert_eq!(parsed.address, "192.168.1.1");
576        assert_eq!(parsed.protocol, Protocol::Unknown("future-protocol".to_string(), true));
577        assert_eq!(parsed.port, 1234);
578        
579        // Test round trip
580        let reconstructed = parsed.to_multiaddr();
581        assert_eq!(reconstructed, multiaddr);
582    }
583    
584    #[test]
585    fn test_unknown_ip_type_parsing() {
586        let multiaddr = "/future-ip/test-address/tcp/8080";
587        let parsed = ParsedMultiaddr::parse(multiaddr).unwrap();
588        
589        assert_eq!(parsed.ip_type, IpType::Unknown("future-ip".to_string()));
590        assert_eq!(parsed.address, "test-address");
591        assert_eq!(parsed.protocol, Protocol::TCP);
592        assert_eq!(parsed.port, 8080);
593        
594        // Test round trip
595        let reconstructed = parsed.to_multiaddr();
596        assert_eq!(reconstructed, multiaddr);
597    }
598    
599    #[test]
600    fn test_extended_protocol_support() {
601        let test_multiaddrs = vec![
602            "/onion/example.onion:80/tcp/8080",
603            "/onion3/example.onion/tls",
604            "/garlic64/garlic-address/noise",
605            "/memory/mem-addr/yamux",
606            "/cid/QmHash/webrtc-direct",
607        ];
608        
609        for multiaddr in test_multiaddrs {
610            let parsed = ParsedMultiaddr::parse(multiaddr).unwrap();
611            let _reconstructed = parsed.to_multiaddr();
612            
613            // Check that we can parse all these formats
614            assert!(parsed.ip_type != IpType::IPv4); // Should be something else
615            
616            println!("✅ Parsed: {} → {:?}", multiaddr, parsed.ip_type);
617        }
618    }
619}