ln_types/
p2p_address.rs

1//! P2P address (`node_id@host:port`)
2//!
3//! This module provides the [`P2PAddress`] type and the related error types.
4
5use core::borrow::Borrow;
6use core::convert::TryFrom;
7use core::str::FromStr;
8use core::fmt;
9#[cfg(feature = "std")]
10use std::io;
11#[cfg(feature = "std")]
12use std::vec::Vec;
13use crate::NodeId;
14
15#[cfg(rust_v_1_77)]
16use core::net;
17#[cfg(not(rust_v_1_77))]
18use std::net;
19
20#[cfg(feature = "alloc")]
21use alloc::{boxed::Box, string::String, borrow::ToOwned, string::ToString};
22
23const LN_DEFAULT_PORT: u16 = 9735;
24
25/// Abstracts over string operations.
26///
27/// This trait enables efficient conversions.
28#[cfg(feature = "alloc")]
29trait StringOps: AsRef<str> + Into<String> {
30    /// Converts given range of `self` into `String`
31    fn into_substring(self, start: usize, end: usize) -> String;
32}
33
34#[cfg(not(feature = "alloc"))]
35trait StringOps: AsRef<str> { }
36
37/// The implementation avoids allocations - whole point of the trait.
38#[cfg(feature = "alloc")]
39impl StringOps for String {
40    fn into_substring(mut self, start: usize, end: usize) -> String {
41        self.replace_range(0..start, "");
42        self.truncate(end - start);
43        self
44    }
45}
46
47impl<'a> StringOps for &'a str {
48    #[cfg(feature = "alloc")]
49    fn into_substring(self, start: usize, end: usize) -> String {
50        self[start..end].to_owned()
51    }
52}
53
54/// Avoids allocations but has to store capacity
55#[cfg(feature = "alloc")]
56impl StringOps for Box<str> {
57    fn into_substring(self, start: usize, end: usize) -> String {
58        String::from(self).into_substring(start, end)
59    }
60}
61
62/// Internal type that can store IP addresses without allocations.
63///
64/// This may be (partially) public in the future.
65#[derive(Clone)]
66enum HostInner {
67    Ip(net::IpAddr),
68    #[cfg(feature = "alloc")]
69    Hostname(String),
70    // TODO: onion
71}
72
73/// Type representing network address of an LN node.
74///
75/// This type can avoid allocations if the value is an IP address.
76///
77/// **Important: consumer code MUST NOT match on this using `Host { .. }` syntax.
78#[derive(Clone)]
79pub struct Host(HostInner);
80
81impl Host {
82    /// Returns true if it's an onion (Tor) adress.
83    pub fn is_onion(&self) -> bool {
84        match &self.0 {
85            #[cfg(feature = "alloc")]
86            HostInner::Hostname(hostname) => hostname.ends_with(".onion"),
87            HostInner::Ip(_) => false,
88        }
89    }
90
91    /// Returns true if it's an IP adress.
92    pub fn is_ip_addr(&self) -> bool {
93        match &self.0 {
94            #[cfg(feature = "alloc")]
95            HostInner::Hostname(_) => false,
96            HostInner::Ip(_) => true,
97        }
98    }
99}
100
101impl fmt::Display for Host {
102    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
103        match &self.0 {
104            HostInner::Ip(addr) => fmt::Display::fmt(&addr, f),
105            #[cfg(feature = "alloc")]
106            HostInner::Hostname(addr) => fmt::Display::fmt(&addr, f),
107        }
108    }
109}
110
111/// Helper struct that can be used to correctly display `host:port`
112///
113/// This is needed because IPv6 addresses need square brackets when displayed as `ip:port` but
114/// square brackets are not used when they are displayed standalone.
115pub struct HostPort<H: Borrow<Host>>(
116    /// Host
117    ///
118    /// You can use `Host`, `&Host` or other smart pointers here.
119    pub H,
120
121    /// Port
122    pub u16,
123);
124
125/// Makes sure to use square brackets around IPv6
126impl<H: Borrow<Host>> fmt::Display for HostPort<H> {
127    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
128        match &self.0.borrow().0 {
129            HostInner::Ip(net::IpAddr::V6(addr)) => write!(f, "[{}]:{}", addr, self.1),
130            _ => write!(f, "{}:{}", self.0.borrow(), self.1),
131        }
132    }
133}
134
135#[cfg(feature = "alloc")]
136impl From<Host> for String {
137    fn from(value: Host) -> Self {
138        match value.0 {
139            HostInner::Ip(ip_addr) => ip_addr.to_string(),
140            #[cfg(feature = "alloc")]
141            HostInner::Hostname(hostname) => hostname,
142        }
143    }
144}
145
146/// This does **not** attempt to resolve a hostname!
147impl TryFrom<Host> for net::IpAddr {
148    type Error = NotIpAddr;
149
150    fn try_from(value: Host) -> Result<Self, Self::Error> {
151        match value.0 {
152            HostInner::Ip(ip_addr) => Ok(ip_addr),
153            #[cfg(feature = "alloc")]
154            HostInner::Hostname(hostname) => Err(NotIpAddr(hostname)),
155        }
156    }
157}
158
159/// Error returned when attempting to *convert* (not resolve) hostname to IP address.
160///
161/// **Important: consumer code MUST NOT match on this using `NotIpAddr { .. }` syntax.
162#[cfg(feature = "alloc")]
163#[derive(Debug)]
164pub struct NotIpAddr(String);
165
166/// Error returned when attempting to *convert* (not resolve) hostname to IP address.
167///
168/// **Important: consumer code MUST NOT match on this using `NotIpAddr { .. }` syntax.
169#[derive(Debug)]
170#[cfg(not(feature = "alloc"))]
171#[non_exhaustive]
172pub struct NotIpAddr;
173
174impl fmt::Display for NotIpAddr {
175    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
176        #[cfg(feature = "alloc")]
177        {
178            write!(f, "the hostname '{}' is not an IP address", self.0)
179        }
180        #[cfg(not(feature = "alloc"))]
181        {
182            write!(f, "the hostname is not an IP address")
183        }
184    }
185}
186
187#[cfg(feature = "std")]
188impl std::error::Error for NotIpAddr {}
189
190/// Parsed Lightning P2P address.
191///
192/// This type stores parsed representation of P2P address usually written in form `node_id@host:port`.
193/// It can efficiently parse and display the address as well as perform various conversions using
194/// external crates.
195///
196/// It also stores host in a way that can avoid allocation if it's **not** a host name.
197/// This also means it works without `alloc` feature however it can not be constructed with a
198/// hostname.
199///
200/// **Serde limitations:** non-human-readable formats are not supported yet as it wasn't decided
201/// what's the best way of doing it. Please state your preference in GitHub issues.
202///
203/// # Example
204///
205/// ```
206/// # #[cfg(feature = "alloc")] {
207/// let marvin_str = "029ef8ee0ba895e2807ac1df1987a7888116c468e70f42e7b089e06811b0e45482@ln-ask.me";
208/// let marvin = marvin_str.parse::<ln_types::P2PAddress>().unwrap();
209/// assert_eq!(marvin.node_id.to_string(), "029ef8ee0ba895e2807ac1df1987a7888116c468e70f42e7b089e06811b0e45482");
210/// assert!(!marvin.host.is_ip_addr());
211/// assert_eq!(marvin.port, 9735);
212/// # }
213/// ```
214#[derive(Clone)]
215pub struct P2PAddress {
216    /// The representation of nodes public key
217    pub node_id: NodeId,
218    /// Network address of the node
219    pub host: Host,
220    /// Network port number of the node
221    pub port: u16,
222}
223
224/// Intermediate representation of host.
225///
226/// This stores range representing host instead of string directly so that it can be returned from
227/// a monomorphic function without requiring allocations.
228enum IpOrHostnamePos {
229    Ip(net::IpAddr),
230    #[cfg(feature = "alloc")]
231    Hostname(usize, usize),
232    #[cfg(not(feature = "alloc"))]
233    Hostname((), ()),
234}
235
236impl P2PAddress {
237    /// Conveniently constructs [`HostPort`].
238    ///
239    /// This can be used when `NodeId` is not needed - e.g. when creating string representation of
240    /// connection information.
241    pub fn as_host_port(&self) -> HostPort<&Host> {
242        HostPort(&self.host, self.port)
243    }
244
245    /// Internal monomorphic parsing method.
246    ///
247    /// This should improve codegen without requiring allocations.
248    fn parse_raw(s: &str) -> Result<(NodeId, IpOrHostnamePos, u16), ParseErrorInner> {
249        let at_pos = s.find('@').ok_or(ParseErrorInner::MissingAtSymbol)?;
250        let (node_id, host_port) = s.split_at(at_pos);
251        let host_port = &host_port[1..];
252        let node_id = node_id.parse().map_err(ParseErrorInner::InvalidNodeId)?;
253        let (host_end, port) = match (host_port.starts_with('[') && host_port.ends_with(']'), host_port.rfind(':')) {
254            // The whole thing is an IPv6, without port
255            (true, _) => (host_port.len(), LN_DEFAULT_PORT),
256            (false, Some(pos)) => (pos, host_port[(pos + 1)..].parse().map_err(ParseErrorInner::InvalidPortNumber)?),
257            (false, None) => (host_port.len(), LN_DEFAULT_PORT),
258        };
259        let host = &host_port[..host_end];
260        let host = match host.parse::<net::Ipv4Addr>() {
261            Ok(ip) => IpOrHostnamePos::Ip(ip.into()),
262            // We have to explicitly parse IPv6 without port to avoid confusing `:`
263            Err(_) if host.starts_with('[') && host.ends_with(']') => {
264                let ip = host_port[1..(host.len() - 1)]
265                    .parse::<net::Ipv6Addr>()
266                    .map_err(ParseErrorInner::InvalidIpv6)?;
267
268                IpOrHostnamePos::Ip(ip.into())
269            },
270            #[cfg(feature = "alloc")]
271            Err(_) => {
272                IpOrHostnamePos::Hostname(at_pos + 1, at_pos + 1 + host_end)
273            },
274            #[cfg(not(feature = "alloc"))]
275            Err(_) => {
276                IpOrHostnamePos::Hostname((), ())
277            },
278        };
279        
280        Ok((node_id, host, port))
281    }
282
283    /// Generic wrapper for parsing that is used to implement parsing from multiple types.
284    fn internal_parse<S: StringOps>(s: S) -> Result<Self, ParseError> {
285        let (node_id, host, port) = match Self::parse_raw(s.as_ref()) {
286            Ok(result) => result,
287            Err(error) => return Err(ParseError {
288                #[cfg(feature = "alloc")]
289                input: s.into(),
290                reason: error,
291            }),
292        };
293        let host = match host {
294            #[cfg(feature = "alloc")]
295            IpOrHostnamePos::Hostname(begin, end) => HostInner::Hostname(s.into_substring(begin, end)),
296            #[cfg(not(feature = "alloc"))]
297            IpOrHostnamePos::Hostname(_, _) => return Err(ParseError { reason: ParseErrorInner::UnsupportedHostname }),
298            IpOrHostnamePos::Ip(ip) => HostInner::Ip(ip),
299        };
300
301        Ok(P2PAddress {
302            node_id,
303            host: Host(host),
304            port,
305        })
306    }
307}
308
309/// Alternative formatting displays node ID in upper case
310impl fmt::Display for P2PAddress {
311    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
312        if f.alternate() {
313            write!(f, "{:X}@{}", self.node_id, HostPort(&self.host, self.port))
314        } else {
315            write!(f, "{:x}@{}", self.node_id, HostPort(&self.host, self.port))
316        }
317    }
318}
319
320/// Same as Display
321impl fmt::Debug for P2PAddress {
322    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
323        fmt::Display::fmt(self, f)
324    }
325}
326
327impl FromStr for P2PAddress {
328    type Err = ParseError;
329
330    #[inline]
331    fn from_str(s: &str) -> Result<Self, Self::Err> {
332        Self::internal_parse(s)
333    }
334}
335
336impl<'a> TryFrom<&'a str> for P2PAddress {
337    type Error = ParseError;
338
339    #[inline]
340    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
341        Self::internal_parse(s)
342    }
343}
344
345#[cfg(feature = "alloc")]
346impl TryFrom<String> for P2PAddress {
347    type Error = ParseError;
348
349    #[inline]
350    fn try_from(s: String) -> Result<Self, Self::Error> {
351        Self::internal_parse(s)
352    }
353}
354
355#[cfg(feature = "alloc")]
356impl TryFrom<Box<str>> for P2PAddress {
357    type Error = ParseError;
358
359    #[inline]
360    fn try_from(s: Box<str>) -> Result<Self, Self::Error> {
361        Self::internal_parse(s)
362    }
363}
364
365/// Error returned when parsing text representation fails.
366///
367/// **Important: consumer code MUST NOT match on this using `ParseError { .. }` syntax.
368#[derive(Debug, Clone)]
369pub struct ParseError {
370    #[cfg(feature = "alloc")]
371    input: String,
372    reason: ParseErrorInner,
373}
374
375impl fmt::Display for ParseError {
376    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
377        #[cfg(feature = "alloc")]
378        {
379            write_err!(f, "failed to parse '{}' as Lightning Network P2P address", self.input; &self.reason)
380        }
381        #[cfg(not(feature = "alloc"))]
382        {
383            write_err!(f, "failed to parse Lightning Network P2P address"; &self.reason)
384        }
385    }
386}
387
388#[cfg(feature = "std")]
389impl std::error::Error for ParseError {
390    #[inline]
391    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
392        if let ParseErrorInner::InvalidNodeId(error) = &self.reason {
393            Some(error)
394        } else {
395            Some(&self.reason)
396        }
397    }
398}
399
400#[derive(Debug, Clone)]
401enum ParseErrorInner {
402    MissingAtSymbol,
403    InvalidNodeId(crate::node_id::ParseError),
404    InvalidPortNumber(core::num::ParseIntError),
405    InvalidIpv6(net::AddrParseError),
406    #[cfg(not(feature = "alloc"))]
407    UnsupportedHostname,
408}
409
410impl fmt::Display for ParseErrorInner {
411    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
412        match self {
413            ParseErrorInner::MissingAtSymbol => f.write_str("missing '@' symbol"),
414            ParseErrorInner::InvalidNodeId(error) => fmt::Display::fmt(error, f),
415            ParseErrorInner::InvalidPortNumber(error) => write_err!(f, "invalid port number"; error),
416            ParseErrorInner::InvalidIpv6(error) => write_err!(f, "invalid IPv6 address"; error),
417            #[cfg(not(feature = "alloc"))]
418            ParseErrorInner::UnsupportedHostname => f.write_str("the address is a hostname which is unsupported in this build (without an allocator)"),
419        }
420    }
421}
422
423#[cfg(feature = "std")]
424impl std::error::Error for ParseErrorInner {
425    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
426        match self {
427            ParseErrorInner::MissingAtSymbol => None,
428            ParseErrorInner::InvalidNodeId(error) => error.source(),
429            ParseErrorInner::InvalidPortNumber(error) => Some(error),
430            ParseErrorInner::InvalidIpv6(error) => Some(error),
431            #[cfg(not(feature = "alloc"))]
432            ParseErrorInner::UnsupportedHostname => None,
433        }
434    }
435}
436
437/// Iterator over socket addresses returned by `to_socket_addrs()`
438///
439/// This is the iterator used in the implementation of [`std::net::ToSocketAddrs`] for [`HostPort`]
440/// and [`P2PAddress`].
441#[cfg(feature = "std")]
442pub struct SocketAddrs {
443    iter: core::iter::Chain<core::option::IntoIter<net::SocketAddr>, std::vec::IntoIter<net::SocketAddr>>
444}
445
446#[cfg(feature = "std")]
447impl Iterator for SocketAddrs {
448    type Item = net::SocketAddr;
449
450    fn next(&mut self) -> Option<Self::Item> {
451        self.iter.next()
452    }
453}
454
455/// Note that onion addresses can never be resolved, you have to use a proxy instead.
456#[cfg(feature = "std")]
457impl<H: Borrow<Host>> std::net::ToSocketAddrs for HostPort<H> {
458    type Iter = SocketAddrs;
459
460    fn to_socket_addrs(&self) -> io::Result<Self::Iter> {
461        if self.0.borrow().is_onion() {
462            return Err(io::Error::new(io::ErrorKind::InvalidInput, ResolveOnion));
463        }
464
465        let iter = match &self.0.borrow().0 {
466            HostInner::Ip(ip_addr) => Some(net::SocketAddr::new(*ip_addr, self.1)).into_iter().chain(Vec::new()),
467            HostInner::Hostname(hostname) => None.into_iter().chain((hostname.as_str(), self.1).to_socket_addrs()?),
468        };
469
470        Ok(SocketAddrs {
471            iter,
472        })
473    }
474}
475
476/// Note that onion addresses can never be resolved, you have to use a proxy instead.
477#[cfg(feature = "std")]
478impl std::net::ToSocketAddrs for P2PAddress {
479    type Iter = SocketAddrs;
480
481    fn to_socket_addrs(&self) -> io::Result<Self::Iter> {
482        HostPort(&self.host, self.port).to_socket_addrs()
483    }
484}
485
486/// Error type returned when attempting to resolve onion address.
487// If this is made public it should be future-proofed like other errors.
488#[derive(Debug)]
489#[cfg(feature = "std")]
490struct ResolveOnion;
491
492#[cfg(feature = "std")]
493impl fmt::Display for ResolveOnion {
494    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
495        f.write_str("attempt to resolve onion address")
496    }
497}
498
499#[cfg(feature = "std")]
500impl std::error::Error for ResolveOnion {}
501
502#[cfg(feature = "parse_arg")]
503mod parse_arg_impl {
504    use core::fmt;
505    use super::P2PAddress;
506
507    impl parse_arg::ParseArgFromStr for P2PAddress {
508        fn describe_type<W: fmt::Write>(mut writer: W) -> fmt::Result {
509            writer.write_str("a Lightning Network address in the form `nodeid@host:port`")
510        }
511    }
512}
513
514#[cfg(feature = "serde")]
515mod serde_impl {
516    use core::fmt;
517    use super::P2PAddress;
518    use serde::{Serialize, Deserialize, Serializer, Deserializer, de::{Visitor, Error}};
519    use core::convert::TryInto;
520
521    #[cfg(feature = "serde_alloc")]
522    use alloc::string::String;
523
524    struct HRVisitor;
525
526    impl<'de> Visitor<'de> for HRVisitor {
527        type Value = P2PAddress;
528
529        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
530            formatter.write_str("a 66 digits long hex string")
531        }
532
533        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> where E: Error {
534            v.try_into().map_err(|error| {
535                E::custom(error)
536            })
537        }
538
539        #[cfg(feature = "serde_alloc")]
540        fn visit_string<E>(self, v: String) -> Result<Self::Value, E> where E: Error {
541            v.try_into().map_err(|error| {
542                E::custom(error)
543            })
544        }
545    }
546
547    /// Serialized as string to human-readable formats.
548    ///
549    /// # Errors
550    ///
551    /// This fails if the format is **not** human-readable because it's not decided how it should
552    /// be done.
553    impl Serialize for P2PAddress {
554        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
555            use serde::ser::Error;
556
557            if serializer.is_human_readable() {
558                serializer.collect_str(self)
559            } else {
560                Err(S::Error::custom("serialization is not yet implemented for non-human-readable formats, please file a request"))
561            }
562        }
563    }
564
565    /// Deserialized as string from human-readable formats.
566    ///
567    /// # Errors
568    ///
569    /// This fails if the format is **not** human-readable because it's not decided how it should
570    /// be done.
571    impl<'de> Deserialize<'de> for P2PAddress {
572        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de> {
573            if deserializer.is_human_readable() {
574                deserializer.deserialize_str(HRVisitor)
575            } else {
576                Err(D::Error::custom("deserialization is not yet implemented for non-human-readable formats, please file a request"))
577            }
578        }
579    }
580}
581
582#[cfg(feature = "postgres-types")]
583mod postgres_impl {
584    use alloc::boxed::Box;
585    use super::P2PAddress;
586    use postgres_types::{ToSql, FromSql, IsNull, Type};
587    use bytes::BytesMut;
588    use std::error::Error;
589
590    /// Stores the value as text (same types as `&str`)
591    impl ToSql for P2PAddress {
592        fn to_sql(&self, _ty: &Type, out: &mut BytesMut) -> Result<IsNull, Box<dyn Error + Send + Sync + 'static>> {
593            use core::fmt::Write;
594
595            write!(out, "{}", self).map(|_| IsNull::No).map_err(|error| Box::new(error) as _)
596        }
597
598        fn accepts(ty: &Type) -> bool {
599            <&str as ToSql>::accepts(ty)
600        }
601
602        postgres_types::to_sql_checked!();
603    }
604
605    /// Retrieves the value as text (same types as `&str`)
606    impl<'a> FromSql<'a> for P2PAddress {
607        fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Send + Sync + 'static>> {
608            <&str>::from_sql(ty, raw)?.parse().map_err(|error| Box::new(error) as _)
609        }
610
611        fn accepts(ty: &Type) -> bool {
612            <&str as FromSql>::accepts(ty)
613        }
614    }
615}
616
617/// Implementations of `slog` traits
618#[cfg(feature = "slog")]
619mod slog_impl {
620    use super::P2PAddress;
621    use slog::{Key, Value, KV, Record, Serializer};
622
623    /// Uses `Display`
624    impl Value for P2PAddress {
625        fn serialize(&self, _rec: &Record, key: Key, serializer: &mut dyn Serializer) -> slog::Result {
626            serializer.emit_arguments(key, &format_args!("{}", self))
627        }
628    }
629
630    /// Serializes each field separately.
631    ///
632    /// The fields are:
633    ///
634    /// * `node_id` - delegates to `NodeId`
635    /// * `host` - `Display`
636    /// * `port` - `emit_u16`
637    impl KV for P2PAddress {
638        fn serialize(&self, rec: &Record, serializer: &mut dyn Serializer) -> slog::Result {
639            // `Key` is a type alias but if `slog/dynamic_keys` feature is enabled it's not
640            #![allow(clippy::useless_conversion)]
641            self.node_id.serialize(rec, Key::from("node_id"), serializer)?;
642            serializer.emit_arguments(Key::from("host"), &format_args!("{}", self.host))?;
643            serializer.emit_u16(Key::from("port"), self.port)?;
644            Ok(())
645        }
646    }
647
648    impl_error_value!(super::ParseError);
649}
650
651#[cfg(test)]
652mod tests {
653    use super::P2PAddress;
654    use alloc::{format, string::ToString};
655
656    #[test]
657    fn empty() {
658        assert!("".parse::<P2PAddress>().is_err());
659    }
660
661    #[test]
662    fn invalid_node_id() {
663        assert!("@example.com".parse::<P2PAddress>().is_err());
664    }
665
666    #[test]
667    fn invalid_port() {
668        assert!("022345678901234567890123456789012345678901234567890123456789abcdef@example.com:foo".parse::<P2PAddress>().is_err());
669    }
670
671    #[test]
672    #[cfg(feature = "alloc")]
673    fn correct_hostname_no_port() {
674        let input = "022345678901234567890123456789012345678901234567890123456789abcdef@example.com";
675        let parsed = input.parse::<P2PAddress>().unwrap();
676        let output = parsed.to_string();
677        let expected = format!("{}{}", input, ":9735");
678        assert_eq!(output, expected);
679    }
680
681    #[test]
682    #[cfg(feature = "alloc")]
683    fn correct_with_hostname_port() {
684        let input = "022345678901234567890123456789012345678901234567890123456789abcdef@example.com:1234";
685        let parsed = input.parse::<P2PAddress>().unwrap();
686        let output = parsed.to_string();
687        assert_eq!(output, input);
688    }
689
690    #[test]
691    fn correct_ipv4_no_port() {
692        let input = "022345678901234567890123456789012345678901234567890123456789abcdef@127.0.0.1";
693        let parsed = input.parse::<P2PAddress>().unwrap();
694        let output = parsed.to_string();
695        let expected = format!("{}{}", input, ":9735");
696        assert_eq!(output, expected);
697    }
698
699    #[test]
700    fn correct_with_ipv4_port() {
701        let input = "022345678901234567890123456789012345678901234567890123456789abcdef@127.0.0.1:1234";
702        let parsed = input.parse::<P2PAddress>().unwrap();
703        let output = parsed.to_string();
704        assert_eq!(output, input);
705    }
706
707    #[test]
708    fn ipv6_no_port() {
709        let input = "022345678901234567890123456789012345678901234567890123456789abcdef@[::1]";
710        let parsed = input.parse::<P2PAddress>().unwrap();
711        let output = parsed.to_string();
712        let expected = format!("{}{}", input, ":9735");
713        assert_eq!(output, expected);
714    }
715
716    #[test]
717    fn ipv6_with_port() {
718        let input = "022345678901234567890123456789012345678901234567890123456789abcdef@[::1]:1234";
719        let parsed = input.parse::<P2PAddress>().unwrap();
720        let output = parsed.to_string();
721        assert_eq!(output, input);
722    }
723
724    chk_err_impl! {
725        parse_p2p_address_error_empty, "", P2PAddress, ["failed to parse '' as Lightning Network P2P address", "missing '@' symbol"], ["failed to parse Lightning Network P2P address", "missing '@' symbol"];
726        parse_p2p_address_error_empty_node_id, "@127.0.0.1", P2PAddress, [
727            "failed to parse '@127.0.0.1' as Lightning Network P2P address",
728            "failed to parse '' as Lightning Network node ID",
729            "invalid length (must be 66 chars)",
730        ], [
731            "failed to parse Lightning Network P2P address",
732            "failed to parse Lightning Network node ID",
733            "invalid length (must be 66 chars)",
734        ];
735    }
736}