Skip to main content

hashtree_cli/webrtc/
types.rs

1//! WebRTC signaling types compatible with iris-client and hashtree-ts
2
3pub use hashtree_network::{
4    decrement_htl_with_policy, should_forward_htl, validate_mesh_frame, HtlMode, HtlPolicy,
5    IceCandidate, MeshNostrFrame, MeshNostrPayload, PeerHTLConfig, PeerPool, PoolConfig,
6    PoolSettings, RequestDispatchConfig, SelectionStrategy, SignalingMessage, TimedSeenSet,
7    BLOB_REQUEST_POLICY, DECREMENT_AT_MAX_PROB, DECREMENT_AT_MIN_PROB, MAX_HTL, MESH_DEFAULT_HTL,
8    MESH_EVENT_POLICY, MESH_MAX_HTL, MESH_PROTOCOL, MESH_PROTOCOL_VERSION,
9};
10use serde::{Deserialize, Serialize};
11
12/// Backward-compatible helper using blob-request policy.
13pub fn decrement_htl(htl: u8, config: &PeerHTLConfig) -> u8 {
14    decrement_htl_with_policy(htl, &BLOB_REQUEST_POLICY, config)
15}
16
17/// Backward-compatible helper for existing call sites.
18pub fn should_forward(htl: u8) -> bool {
19    should_forward_htl(htl)
20}
21
22/// Event kind for WebRTC signaling (ephemeral kind 25050)
23/// All signaling uses this kind - hellos use #l tag, directed use gift wrap
24pub const WEBRTC_KIND: u64 = 25050;
25
26/// Tag for hello messages (broadcast discovery)
27pub const HELLO_TAG: &str = "hello";
28
29/// Legacy tag for WebRTC signaling messages (kept for compatibility)
30pub const WEBRTC_TAG: &str = "webrtc";
31
32/// Generate a UUID for peer identification
33pub fn generate_uuid() -> String {
34    use rand::Rng;
35    let mut rng = rand::thread_rng();
36    format!(
37        "{}{}",
38        (0..15)
39            .map(|_| char::from_digit(rng.gen_range(0..36), 36).unwrap())
40            .collect::<String>(),
41        (0..15)
42            .map(|_| char::from_digit(rng.gen_range(0..36), 36).unwrap())
43            .collect::<String>()
44    )
45}
46
47fn configured_peer_uuid() -> Option<String> {
48    std::env::var("HTREE_PEER_UUID")
49        .ok()
50        .map(|value| value.trim().to_string())
51        .filter(|value| !value.is_empty())
52}
53
54/// Peer identifier combining pubkey and session UUID
55#[derive(Debug, Clone, PartialEq, Eq, Hash)]
56pub struct PeerId {
57    pub pubkey: String,
58    pub uuid: String,
59}
60
61impl PeerId {
62    pub fn new(pubkey: String, uuid: Option<String>) -> Self {
63        Self {
64            pubkey,
65            uuid: uuid
66                .or_else(configured_peer_uuid)
67                .unwrap_or_else(generate_uuid),
68        }
69    }
70
71    pub fn from_string(s: &str) -> Option<Self> {
72        let parts: Vec<&str> = s.split(':').collect();
73        if parts.len() == 2 {
74            Some(Self {
75                pubkey: parts[0].to_string(),
76                uuid: parts[1].to_string(),
77            })
78        } else {
79            None
80        }
81    }
82
83    pub fn short(&self) -> String {
84        format!(
85            "{}:{}",
86            &self.pubkey[..8.min(self.pubkey.len())],
87            &self.uuid[..6.min(self.uuid.len())]
88        )
89    }
90}
91
92impl std::fmt::Display for PeerId {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        write!(f, "{}:{}", self.pubkey, self.uuid)
95    }
96}
97
98/// Configuration for WebRTC manager
99#[derive(Clone)]
100pub struct WebRTCConfig {
101    /// Nostr relays for signaling
102    pub relays: Vec<String>,
103    /// Whether negotiated WebRTC signaling should run at all.
104    pub signaling_enabled: bool,
105    /// Maximum outbound connections (legacy, use pools instead)
106    pub max_outbound: usize,
107    /// Maximum inbound connections (legacy, use pools instead)
108    pub max_inbound: usize,
109    /// Hello message interval in milliseconds
110    pub hello_interval_ms: u64,
111    /// Message timeout in milliseconds
112    pub message_timeout_ms: u64,
113    /// STUN servers for NAT traversal
114    pub stun_servers: Vec<String>,
115    /// Enable debug logging
116    pub debug: bool,
117    /// Optional LAN multicast transport for offline discovery + root lookup.
118    pub multicast: super::multicast::MulticastConfig,
119    /// Optional Android Wi-Fi Aware nearby discovery/signaling bus.
120    pub wifi_aware: super::wifi_aware::WifiAwareConfig,
121    /// Optional native Bluetooth peer transport.
122    pub bluetooth: super::bluetooth::BluetoothConfig,
123    /// Pool settings for follows and other peers
124    pub pools: PoolSettings,
125    /// Retrieval peer selection strategy (shared with simulation).
126    pub request_selection_strategy: SelectionStrategy,
127    /// Whether fairness constraints are enabled for retrieval peer selection.
128    pub request_fairness_enabled: bool,
129    /// Hedged request dispatch policy for retrieval (shared with simulation).
130    pub request_dispatch: RequestDispatchConfig,
131}
132
133impl Default for WebRTCConfig {
134    fn default() -> Self {
135        Self {
136            relays: vec![
137                "wss://relay.damus.io".to_string(),
138                "wss://relay.primal.net".to_string(),
139                "wss://temp.iris.to".to_string(),
140                "wss://relay.snort.social".to_string(),
141            ],
142            signaling_enabled: true,
143            max_outbound: 6,
144            max_inbound: 6,
145            hello_interval_ms: 3000,
146            message_timeout_ms: 15000,
147            stun_servers: vec![
148                "stun:stun.iris.to:3478".to_string(),
149                "stun:stun.l.google.com:19302".to_string(),
150                "stun:stun.cloudflare.com:3478".to_string(),
151            ],
152            debug: false,
153            multicast: super::multicast::MulticastConfig::default(),
154            wifi_aware: super::wifi_aware::WifiAwareConfig::default(),
155            bluetooth: super::bluetooth::BluetoothConfig::default(),
156            pools: PoolSettings::default(),
157            request_selection_strategy: SelectionStrategy::TitForTat,
158            request_fairness_enabled: true,
159            request_dispatch: RequestDispatchConfig {
160                initial_fanout: 2,
161                hedge_fanout: 1,
162                max_fanout: 8,
163                hedge_interval_ms: 120,
164            },
165        }
166    }
167}
168
169pub type PeerRouterConfig = WebRTCConfig;
170
171/// Peer connection status
172#[derive(Debug, Clone)]
173pub struct PeerStatus {
174    pub peer_id: String,
175    pub pubkey: String,
176    pub state: String,
177    pub direction: PeerDirection,
178    pub connected_at: Option<std::time::Instant>,
179    pub pool: PeerPool,
180}
181
182/// Direction of peer connection
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum PeerDirection {
185    Inbound,
186    Outbound,
187}
188
189/// Peer state change event for signaling layer notification
190#[derive(Debug, Clone)]
191pub enum PeerStateEvent {
192    /// Peer connection succeeded
193    Connected(PeerId),
194    /// Peer connection failed
195    Failed(PeerId),
196    /// Peer disconnected
197    Disconnected(PeerId),
198}
199
200impl std::fmt::Display for PeerDirection {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        match self {
203            PeerDirection::Inbound => write!(f, "inbound"),
204            PeerDirection::Outbound => write!(f, "outbound"),
205        }
206    }
207}
208
209/// Message type bytes (prefix before MessagePack body)
210pub const MSG_TYPE_REQUEST: u8 = 0x00;
211pub const MSG_TYPE_RESPONSE: u8 = 0x01;
212pub const MSG_TYPE_QUOTE_REQUEST: u8 = 0x02;
213pub const MSG_TYPE_QUOTE_RESPONSE: u8 = 0x03;
214pub const MSG_TYPE_PAYMENT: u8 = 0x04;
215pub const MSG_TYPE_PAYMENT_ACK: u8 = 0x05;
216pub const MSG_TYPE_CHUNK: u8 = 0x06;
217
218/// Hashtree data channel protocol messages
219/// Shared between WebRTC data channels and WebSocket transport
220///
221/// Wire format: [type byte][msgpack body]
222/// Request:  [0x00][msgpack: {h: bytes32, htl?: u8, q?: u64}]
223/// Response: [0x01][msgpack: {h: bytes32, d: bytes}]
224/// QuoteRequest:  [0x02][msgpack: {h: bytes32, p: u64, t: u32, m?: string}]
225/// QuoteResponse: [0x03][msgpack: {h: bytes32, a: bool, q?: u64, p?: u64, t?: u32, m?: string}]
226/// Payment:       [0x04][msgpack: {h: bytes32, q: u64, c: u32, p: u64, m?: string, tok: string}]
227/// PaymentAck:    [0x05][msgpack: {h: bytes32, q: u64, c: u32, a: bool, e?: string}]
228/// Chunk:         [0x06][msgpack: {h: bytes32, q: u64, c: u32, n: u32, p: u64, d: bytes}]
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct DataRequest {
232    #[serde(with = "serde_bytes")]
233    pub h: Vec<u8>, // 32-byte hash
234    #[serde(default = "default_htl", skip_serializing_if = "is_max_htl")]
235    pub htl: u8,
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub q: Option<u64>,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct DataResponse {
242    #[serde(with = "serde_bytes")]
243    pub h: Vec<u8>, // 32-byte hash
244    #[serde(with = "serde_bytes")]
245    pub d: Vec<u8>, // Data
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct DataQuoteRequest {
250    #[serde(with = "serde_bytes")]
251    pub h: Vec<u8>,
252    pub p: u64,
253    pub t: u32,
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub m: Option<String>,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct DataQuoteResponse {
260    #[serde(with = "serde_bytes")]
261    pub h: Vec<u8>,
262    pub a: bool,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub q: Option<u64>,
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub p: Option<u64>,
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub t: Option<u32>,
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub m: Option<String>,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct DataPayment {
275    #[serde(with = "serde_bytes")]
276    pub h: Vec<u8>,
277    pub q: u64,
278    pub c: u32,
279    pub p: u64,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub m: Option<String>,
282    pub tok: String,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct DataPaymentAck {
287    #[serde(with = "serde_bytes")]
288    pub h: Vec<u8>,
289    pub q: u64,
290    pub c: u32,
291    pub a: bool,
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub e: Option<String>,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct DataChunk {
298    #[serde(with = "serde_bytes")]
299    pub h: Vec<u8>,
300    pub q: u64,
301    pub c: u32,
302    pub n: u32,
303    pub p: u64,
304    #[serde(with = "serde_bytes")]
305    pub d: Vec<u8>,
306}
307
308#[derive(Debug, Clone)]
309pub enum DataMessage {
310    Request(DataRequest),
311    Response(DataResponse),
312    QuoteRequest(DataQuoteRequest),
313    QuoteResponse(DataQuoteResponse),
314    Payment(DataPayment),
315    PaymentAck(DataPaymentAck),
316    Chunk(DataChunk),
317}
318
319fn default_htl() -> u8 {
320    BLOB_REQUEST_POLICY.max_htl
321}
322
323fn is_max_htl(htl: &u8) -> bool {
324    *htl == BLOB_REQUEST_POLICY.max_htl
325}
326
327/// Encode a request to wire format: [0x00][msgpack body]
328/// Uses named fields for cross-language compatibility with TypeScript
329pub fn encode_request(req: &DataRequest) -> Result<Vec<u8>, rmp_serde::encode::Error> {
330    let body = rmp_serde::to_vec_named(req)?;
331    let mut result = Vec::with_capacity(1 + body.len());
332    result.push(MSG_TYPE_REQUEST);
333    result.extend(body);
334    Ok(result)
335}
336
337/// Encode a response to wire format: [0x01][msgpack body]
338/// Uses named fields for cross-language compatibility with TypeScript
339pub fn encode_response(res: &DataResponse) -> Result<Vec<u8>, rmp_serde::encode::Error> {
340    let body = rmp_serde::to_vec_named(res)?;
341    let mut result = Vec::with_capacity(1 + body.len());
342    result.push(MSG_TYPE_RESPONSE);
343    result.extend(body);
344    Ok(result)
345}
346
347/// Encode a quote request to wire format: [0x02][msgpack body]
348pub fn encode_quote_request(req: &DataQuoteRequest) -> Result<Vec<u8>, rmp_serde::encode::Error> {
349    let body = rmp_serde::to_vec_named(req)?;
350    let mut result = Vec::with_capacity(1 + body.len());
351    result.push(MSG_TYPE_QUOTE_REQUEST);
352    result.extend(body);
353    Ok(result)
354}
355
356/// Encode a quote response to wire format: [0x03][msgpack body]
357pub fn encode_quote_response(res: &DataQuoteResponse) -> Result<Vec<u8>, rmp_serde::encode::Error> {
358    let body = rmp_serde::to_vec_named(res)?;
359    let mut result = Vec::with_capacity(1 + body.len());
360    result.push(MSG_TYPE_QUOTE_RESPONSE);
361    result.extend(body);
362    Ok(result)
363}
364
365pub fn encode_payment(req: &DataPayment) -> Result<Vec<u8>, rmp_serde::encode::Error> {
366    let body = rmp_serde::to_vec_named(req)?;
367    let mut result = Vec::with_capacity(1 + body.len());
368    result.push(MSG_TYPE_PAYMENT);
369    result.extend(body);
370    Ok(result)
371}
372
373pub fn encode_payment_ack(res: &DataPaymentAck) -> Result<Vec<u8>, rmp_serde::encode::Error> {
374    let body = rmp_serde::to_vec_named(res)?;
375    let mut result = Vec::with_capacity(1 + body.len());
376    result.push(MSG_TYPE_PAYMENT_ACK);
377    result.extend(body);
378    Ok(result)
379}
380
381pub fn encode_chunk(chunk: &DataChunk) -> Result<Vec<u8>, rmp_serde::encode::Error> {
382    let body = rmp_serde::to_vec_named(chunk)?;
383    let mut result = Vec::with_capacity(1 + body.len());
384    result.push(MSG_TYPE_CHUNK);
385    result.extend(body);
386    Ok(result)
387}
388
389/// Parse a wire format message
390pub fn parse_message(data: &[u8]) -> Result<DataMessage, rmp_serde::decode::Error> {
391    if data.is_empty() {
392        return Err(rmp_serde::decode::Error::LengthMismatch(0));
393    }
394
395    let msg_type = data[0];
396    let body = &data[1..];
397
398    match msg_type {
399        MSG_TYPE_REQUEST => {
400            let req: DataRequest = rmp_serde::from_slice(body)?;
401            Ok(DataMessage::Request(req))
402        }
403        MSG_TYPE_RESPONSE => {
404            let res: DataResponse = rmp_serde::from_slice(body)?;
405            Ok(DataMessage::Response(res))
406        }
407        MSG_TYPE_QUOTE_REQUEST => {
408            let req: DataQuoteRequest = rmp_serde::from_slice(body)?;
409            Ok(DataMessage::QuoteRequest(req))
410        }
411        MSG_TYPE_QUOTE_RESPONSE => {
412            let res: DataQuoteResponse = rmp_serde::from_slice(body)?;
413            Ok(DataMessage::QuoteResponse(res))
414        }
415        MSG_TYPE_PAYMENT => {
416            let req: DataPayment = rmp_serde::from_slice(body)?;
417            Ok(DataMessage::Payment(req))
418        }
419        MSG_TYPE_PAYMENT_ACK => {
420            let res: DataPaymentAck = rmp_serde::from_slice(body)?;
421            Ok(DataMessage::PaymentAck(res))
422        }
423        MSG_TYPE_CHUNK => {
424            let chunk: DataChunk = rmp_serde::from_slice(body)?;
425            Ok(DataMessage::Chunk(chunk))
426        }
427        _ => Err(rmp_serde::decode::Error::LengthMismatch(msg_type as u32)),
428    }
429}
430
431/// Convert hash to hex string for logging/map keys
432pub fn hash_to_hex(hash: &[u8]) -> String {
433    hash.iter().map(|b| format!("{:02x}", b)).collect()
434}
435
436/// Encode a DataMessage to wire format (deprecated - use encode_request/encode_response)
437pub fn encode_message(msg: &DataMessage) -> Result<Vec<u8>, rmp_serde::encode::Error> {
438    match msg {
439        DataMessage::Request(req) => encode_request(req),
440        DataMessage::Response(res) => encode_response(res),
441        DataMessage::QuoteRequest(req) => encode_quote_request(req),
442        DataMessage::QuoteResponse(res) => encode_quote_response(res),
443        DataMessage::Payment(req) => encode_payment(req),
444        DataMessage::PaymentAck(res) => encode_payment_ack(res),
445        DataMessage::Chunk(chunk) => encode_chunk(chunk),
446    }
447}