alpine_protocol_sdk/
discovery.rs

1use std::{
2    fmt, io,
3    net::{SocketAddr, UdpSocket},
4    time::Duration,
5};
6
7use alpine::messages::{DiscoveryReply, DiscoveryRequest};
8use rand::{rngs::OsRng, RngCore};
9use serde_cbor;
10
11/// Options used to configure the blocking discovery helper.
12pub struct DiscoveryClientOptions {
13    pub remote_addr: SocketAddr,
14    pub local_addr: SocketAddr,
15    pub timeout: Duration,
16}
17
18impl DiscoveryClientOptions {
19    /// Creates options with the provided remote socket and a default timeout.
20    pub fn new(remote_addr: SocketAddr, local_addr: SocketAddr, timeout: Duration) -> Self {
21        Self {
22            remote_addr,
23            local_addr,
24            timeout,
25        }
26    }
27}
28
29/// Errors that can happen while sending or receiving discovery payloads.
30#[derive(Debug)]
31pub enum DiscoveryError {
32    Io(io::Error),
33    Decode(serde_cbor::Error),
34    Timeout,
35}
36
37impl fmt::Display for DiscoveryError {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            DiscoveryError::Io(err) => write!(f, "io error: {}", err),
41            DiscoveryError::Decode(err) => write!(f, "cbors serialization error: {}", err),
42            DiscoveryError::Timeout => write!(f, "discovery timed out"),
43        }
44    }
45}
46
47impl std::error::Error for DiscoveryError {}
48
49impl From<io::Error> for DiscoveryError {
50    fn from(err: io::Error) -> Self {
51        match err.kind() {
52            io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock => DiscoveryError::Timeout,
53            _ => DiscoveryError::Io(err),
54        }
55    }
56}
57
58impl From<serde_cbor::Error> for DiscoveryError {
59    fn from(err: serde_cbor::Error) -> Self {
60        DiscoveryError::Decode(err)
61    }
62}
63
64/// The outcome of a discovery request.
65pub struct DiscoveryOutcome {
66    pub reply: DiscoveryReply,
67    pub peer: SocketAddr,
68}
69
70/// Stateless discovery helper that wraps the protocol request/response models.
71pub struct DiscoveryClient {
72    socket: UdpSocket,
73    remote_addr: SocketAddr,
74}
75
76impl DiscoveryClient {
77    /// Creates a client that will send discovery packets to `remote_addr`.
78    pub fn new(options: DiscoveryClientOptions) -> Result<Self, DiscoveryError> {
79        let socket = UdpSocket::bind(options.local_addr)?;
80        socket.set_read_timeout(Some(options.timeout))?;
81        Ok(Self {
82            socket,
83            remote_addr: options.remote_addr,
84        })
85    }
86
87    /// Sends a discovery payload with the requested capability names and waits for a reply.
88    pub fn discover(&self, requested: &[String]) -> Result<DiscoveryOutcome, DiscoveryError> {
89        let mut nonce = vec![0u8; 32];
90        OsRng.fill_bytes(&mut nonce);
91        let request = DiscoveryRequest::new(requested.to_vec(), nonce.clone());
92        let payload = serde_cbor::to_vec(&request)?;
93        self.socket.send_to(&payload, self.remote_addr)?;
94
95        let mut buf = vec![0u8; 2048];
96        let (len, peer) = self.socket.recv_from(&mut buf)?;
97        let reply: DiscoveryReply = serde_cbor::from_slice(&buf[..len])?;
98        Ok(DiscoveryOutcome { reply, peer })
99    }
100}