//! Peer configuration types. //! //! Known peer definitions with transport addresses and connection policies. use serde::{Deserialize, Serialize}; /// Connection policy for a peer. /// /// Determines when and how to establish a connection to a peer. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ConnectPolicy { /// Connect to this peer automatically on node startup. /// This is the only policy supported in the initial implementation. #[default] AutoConnect, /// Connect only when traffic needs to be routed through this peer (future). OnDemand, /// Wait for explicit API call to connect (future). Manual, } /// A transport-specific address for reaching a peer. /// /// Each peer can have multiple addresses across different transports, /// allowing fallback if one transport is unavailable. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct PeerAddress { /// Transport type (e.g., "udp", "tor", "ethernet"). pub transport: String, /// Transport-specific address string. /// /// Format depends on transport type: /// - UDP/TCP: "host:port" — IP address or DNS hostname /// (e.g., "192.168.1.1:2121" or "peer1.example.com:2121") /// - Ethernet: "interface/mac" (e.g., "eth0/aa:bb:cc:dd:ee:ff") pub addr: String, /// Priority for address selection (lower = preferred). /// When multiple addresses are available, lower priority addresses /// are tried first. #[serde(default = "default_priority")] pub priority: u8, /// Wall-clock observation timestamp (Unix ms) for ranking by recency. /// /// `None` means "no freshness signal", typically an operator-edited /// static config. The dialer sorts candidates by this field descending /// so freshly observed overlay or runtime hints can be tried before stale /// static addresses. This field is runtime-only and is ignored when /// comparing peer-address lists for config changes. #[serde(default, skip_serializing_if = "Option::is_none", skip_deserializing)] pub seen_at_ms: Option, } impl PartialEq for PeerAddress { fn eq(&self, other: &Self) -> bool { self.transport == other.transport && self.addr == other.addr && self.priority == other.priority } } impl Eq for PeerAddress {} fn default_priority() -> u8 { 100 } fn default_auto_reconnect() -> bool { true } impl PeerAddress { /// Create a new peer address. pub fn new(transport: impl Into, addr: impl Into) -> Self { Self { transport: transport.into(), addr: addr.into(), priority: default_priority(), seen_at_ms: None, } } /// Create a new peer address with priority. pub fn with_priority( transport: impl Into, addr: impl Into, priority: u8, ) -> Self { Self { transport: transport.into(), addr: addr.into(), priority, seen_at_ms: None, } } /// Tag this address with a freshness timestamp for source-agnostic /// candidate ranking. pub fn with_seen_at_ms(mut self, seen_at_ms: u64) -> Self { self.seen_at_ms = Some(seen_at_ms); self } } /// Configuration for a known peer. /// /// Peers are identified by their Nostr public key (npub) and can have /// multiple transport addresses for reaching them. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct PeerConfig { /// The peer's Nostr public key in npub (bech32) or hex format. pub npub: String, /// Human-readable alias for the peer (optional). #[serde(default, skip_serializing_if = "Option::is_none")] pub alias: Option, /// Transport addresses for reaching this peer. /// /// At least one address is required unless `via_nostr` is `true`, /// in which case the address list may be empty and endpoints are /// resolved from the peer's Nostr advert at dial time. #[serde(default)] pub addresses: Vec, /// Connection policy for this peer. #[serde(default)] pub connect_policy: ConnectPolicy, /// Whether to automatically reconnect after link-dead removal. /// When true (default), the node will retry connecting with exponential /// backoff after MMP removes this peer due to liveness timeout. #[serde(default = "default_auto_reconnect")] pub auto_reconnect: bool, /// Whether to append Nostr-advertised endpoints when dialing this peer. /// /// Static addresses are still attempted first; advert-derived endpoints are /// appended as fallback candidates. #[serde(default)] pub via_nostr: bool, } impl Default for PeerConfig { fn default() -> Self { Self { npub: String::new(), alias: None, addresses: Vec::new(), connect_policy: ConnectPolicy::default(), auto_reconnect: default_auto_reconnect(), via_nostr: false, } } } impl PeerConfig { /// Create a new peer config with a single address. pub fn new( npub: impl Into, transport: impl Into, addr: impl Into, ) -> Self { Self { npub: npub.into(), alias: None, addresses: vec![PeerAddress::new(transport, addr)], connect_policy: ConnectPolicy::default(), auto_reconnect: default_auto_reconnect(), via_nostr: false, } } /// Set an alias for the peer. pub fn with_alias(mut self, alias: impl Into) -> Self { self.alias = Some(alias.into()); self } /// Add an additional address for the peer. pub fn with_address(mut self, addr: PeerAddress) -> Self { self.addresses.push(addr); self } /// Get addresses sorted by priority (lowest first). pub fn addresses_by_priority(&self) -> Vec<&PeerAddress> { let mut addrs: Vec<_> = self.addresses.iter().collect(); addrs.sort_by_key(|a| a.priority); addrs } /// Check if this peer should auto-connect on startup. pub fn is_auto_connect(&self) -> bool { matches!(self.connect_policy, ConnectPolicy::AutoConnect) } }