Skip to main content

hashtree_cli/webrtc/
types.rs

1//! WebRTC signaling types compatible with iris-client and hashtree-ts
2
3pub use hashtree_webrtc::{
4    decrement_htl_with_policy, should_forward_htl, validate_mesh_frame, HtlMode, HtlPolicy,
5    MeshNostrFrame, MeshNostrPayload, PeerHTLConfig, RequestDispatchConfig, SelectionStrategy,
6    TimedSeenSet, BLOB_REQUEST_POLICY, DECREMENT_AT_MAX_PROB, DECREMENT_AT_MIN_PROB, MAX_HTL,
7    MESH_DEFAULT_HTL, MESH_EVENT_POLICY, MESH_MAX_HTL, MESH_PROTOCOL, MESH_PROTOCOL_VERSION,
8};
9use serde::{Deserialize, Serialize};
10
11/// Backward-compatible helper using blob-request policy.
12pub fn decrement_htl(htl: u8, config: &PeerHTLConfig) -> u8 {
13    decrement_htl_with_policy(htl, &BLOB_REQUEST_POLICY, config)
14}
15
16/// Backward-compatible helper for existing call sites.
17pub fn should_forward(htl: u8) -> bool {
18    should_forward_htl(htl)
19}
20
21/// Event kind for WebRTC signaling (ephemeral kind 25050)
22/// All signaling uses this kind - hellos use #l tag, directed use gift wrap
23pub const WEBRTC_KIND: u64 = 25050;
24
25/// Tag for hello messages (broadcast discovery)
26pub const HELLO_TAG: &str = "hello";
27
28/// Legacy tag for WebRTC signaling messages (kept for compatibility)
29pub const WEBRTC_TAG: &str = "webrtc";
30
31/// Generate a UUID for peer identification
32pub fn generate_uuid() -> String {
33    use rand::Rng;
34    let mut rng = rand::thread_rng();
35    format!(
36        "{}{}",
37        (0..15)
38            .map(|_| char::from_digit(rng.gen_range(0..36), 36).unwrap())
39            .collect::<String>(),
40        (0..15)
41            .map(|_| char::from_digit(rng.gen_range(0..36), 36).unwrap())
42            .collect::<String>()
43    )
44}
45
46/// Peer identifier combining pubkey and session UUID
47#[derive(Debug, Clone, PartialEq, Eq, Hash)]
48pub struct PeerId {
49    pub pubkey: String,
50    pub uuid: String,
51}
52
53impl PeerId {
54    pub fn new(pubkey: String, uuid: Option<String>) -> Self {
55        Self {
56            pubkey,
57            uuid: uuid.unwrap_or_else(generate_uuid),
58        }
59    }
60
61    pub fn from_string(s: &str) -> Option<Self> {
62        let parts: Vec<&str> = s.split(':').collect();
63        if parts.len() == 2 {
64            Some(Self {
65                pubkey: parts[0].to_string(),
66                uuid: parts[1].to_string(),
67            })
68        } else {
69            None
70        }
71    }
72
73    pub fn short(&self) -> String {
74        format!(
75            "{}:{}",
76            &self.pubkey[..8.min(self.pubkey.len())],
77            &self.uuid[..6.min(self.uuid.len())]
78        )
79    }
80}
81
82impl std::fmt::Display for PeerId {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        write!(f, "{}:{}", self.pubkey, self.uuid)
85    }
86}
87
88/// Hello message for peer discovery
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct HelloMessage {
91    #[serde(rename = "type")]
92    pub msg_type: String,
93    #[serde(rename = "peerId")]
94    pub peer_id: String,
95}
96
97/// WebRTC offer message
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct OfferMessage {
100    #[serde(rename = "type")]
101    pub msg_type: String,
102    pub offer: serde_json::Value,
103    pub recipient: String,
104    #[serde(rename = "peerId")]
105    pub peer_id: String,
106}
107
108/// WebRTC answer message
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct AnswerMessage {
111    #[serde(rename = "type")]
112    pub msg_type: String,
113    pub answer: serde_json::Value,
114    pub recipient: String,
115    #[serde(rename = "peerId")]
116    pub peer_id: String,
117}
118
119/// ICE candidate message
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct CandidateMessage {
122    #[serde(rename = "type")]
123    pub msg_type: String,
124    pub candidate: serde_json::Value,
125    pub recipient: String,
126    #[serde(rename = "peerId")]
127    pub peer_id: String,
128}
129
130/// Batched ICE candidates message (hashtree-ts extension)
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct CandidatesMessage {
133    #[serde(rename = "type")]
134    pub msg_type: String,
135    pub candidates: Vec<serde_json::Value>,
136    pub recipient: String,
137    #[serde(rename = "peerId")]
138    pub peer_id: String,
139}
140
141/// All signaling message types
142#[derive(Debug, Clone, Serialize, Deserialize)]
143#[serde(tag = "type")]
144pub enum SignalingMessage {
145    #[serde(rename = "hello")]
146    Hello {
147        #[serde(rename = "peerId")]
148        peer_id: String,
149    },
150    #[serde(rename = "offer")]
151    Offer {
152        offer: serde_json::Value,
153        recipient: String,
154        #[serde(rename = "peerId")]
155        peer_id: String,
156    },
157    #[serde(rename = "answer")]
158    Answer {
159        answer: serde_json::Value,
160        recipient: String,
161        #[serde(rename = "peerId")]
162        peer_id: String,
163    },
164    #[serde(rename = "candidate")]
165    Candidate {
166        candidate: serde_json::Value,
167        recipient: String,
168        #[serde(rename = "peerId")]
169        peer_id: String,
170    },
171    #[serde(rename = "candidates")]
172    Candidates {
173        candidates: Vec<serde_json::Value>,
174        recipient: String,
175        #[serde(rename = "peerId")]
176        peer_id: String,
177    },
178}
179
180impl SignalingMessage {
181    pub fn msg_type(&self) -> &str {
182        match self {
183            SignalingMessage::Hello { .. } => "hello",
184            SignalingMessage::Offer { .. } => "offer",
185            SignalingMessage::Answer { .. } => "answer",
186            SignalingMessage::Candidate { .. } => "candidate",
187            SignalingMessage::Candidates { .. } => "candidates",
188        }
189    }
190
191    pub fn recipient(&self) -> Option<&str> {
192        match self {
193            SignalingMessage::Hello { .. } => None,
194            SignalingMessage::Offer { recipient, .. } => Some(recipient),
195            SignalingMessage::Answer { recipient, .. } => Some(recipient),
196            SignalingMessage::Candidate { recipient, .. } => Some(recipient),
197            SignalingMessage::Candidates { recipient, .. } => Some(recipient),
198        }
199    }
200
201    pub fn peer_id(&self) -> &str {
202        match self {
203            SignalingMessage::Hello { peer_id } => peer_id,
204            SignalingMessage::Offer { peer_id, .. } => peer_id,
205            SignalingMessage::Answer { peer_id, .. } => peer_id,
206            SignalingMessage::Candidate { peer_id, .. } => peer_id,
207            SignalingMessage::Candidates { peer_id, .. } => peer_id,
208        }
209    }
210
211    pub fn hello(peer_id: &str) -> Self {
212        SignalingMessage::Hello {
213            peer_id: peer_id.to_string(),
214        }
215    }
216
217    pub fn offer(offer: serde_json::Value, recipient: &str, peer_id: &str) -> Self {
218        SignalingMessage::Offer {
219            offer,
220            recipient: recipient.to_string(),
221            peer_id: peer_id.to_string(),
222        }
223    }
224
225    pub fn answer(answer: serde_json::Value, recipient: &str, peer_id: &str) -> Self {
226        SignalingMessage::Answer {
227            answer,
228            recipient: recipient.to_string(),
229            peer_id: peer_id.to_string(),
230        }
231    }
232
233    pub fn candidate(candidate: serde_json::Value, recipient: &str, peer_id: &str) -> Self {
234        SignalingMessage::Candidate {
235            candidate,
236            recipient: recipient.to_string(),
237            peer_id: peer_id.to_string(),
238        }
239    }
240}
241
242/// Configuration for WebRTC manager
243#[derive(Clone)]
244pub struct WebRTCConfig {
245    /// Nostr relays for signaling
246    pub relays: Vec<String>,
247    /// Maximum outbound connections (legacy, use pools instead)
248    pub max_outbound: usize,
249    /// Maximum inbound connections (legacy, use pools instead)
250    pub max_inbound: usize,
251    /// Hello message interval in milliseconds
252    pub hello_interval_ms: u64,
253    /// Message timeout in milliseconds
254    pub message_timeout_ms: u64,
255    /// STUN servers for NAT traversal
256    pub stun_servers: Vec<String>,
257    /// Enable debug logging
258    pub debug: bool,
259    /// Pool settings for follows and other peers
260    pub pools: PoolSettings,
261    /// Retrieval peer selection strategy (shared with simulation).
262    pub request_selection_strategy: SelectionStrategy,
263    /// Whether fairness constraints are enabled for retrieval peer selection.
264    pub request_fairness_enabled: bool,
265    /// Hedged request dispatch policy for retrieval (shared with simulation).
266    pub request_dispatch: RequestDispatchConfig,
267}
268
269impl Default for WebRTCConfig {
270    fn default() -> Self {
271        Self {
272            relays: vec![
273                "wss://relay.damus.io".to_string(),
274                "wss://relay.primal.net".to_string(),
275                "wss://nos.lol".to_string(),
276                "wss://temp.iris.to".to_string(),
277                "wss://relay.snort.social".to_string(),
278            ],
279            max_outbound: 6,
280            max_inbound: 6,
281            hello_interval_ms: 3000,
282            message_timeout_ms: 15000,
283            stun_servers: vec![
284                "stun:stun.iris.to:3478".to_string(),
285                "stun:stun.l.google.com:19302".to_string(),
286                "stun:stun.cloudflare.com:3478".to_string(),
287            ],
288            debug: false,
289            pools: PoolSettings::default(),
290            request_selection_strategy: SelectionStrategy::TitForTat,
291            request_fairness_enabled: true,
292            request_dispatch: RequestDispatchConfig {
293                initial_fanout: 2,
294                hedge_fanout: 1,
295                max_fanout: 8,
296                hedge_interval_ms: 120,
297            },
298        }
299    }
300}
301
302/// Peer connection status
303#[derive(Debug, Clone)]
304pub struct PeerStatus {
305    pub peer_id: String,
306    pub pubkey: String,
307    pub state: String,
308    pub direction: PeerDirection,
309    pub connected_at: Option<std::time::Instant>,
310    pub pool: PeerPool,
311}
312
313/// Direction of peer connection
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315pub enum PeerDirection {
316    Inbound,
317    Outbound,
318}
319
320/// Peer state change event for signaling layer notification
321#[derive(Debug, Clone)]
322pub enum PeerStateEvent {
323    /// Peer connection succeeded
324    Connected(PeerId),
325    /// Peer connection failed
326    Failed(PeerId),
327    /// Peer disconnected
328    Disconnected(PeerId),
329}
330
331/// Pool type for peer classification
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
333pub enum PeerPool {
334    /// Users in social graph (followed or followers)
335    Follows,
336    /// Everyone else
337    Other,
338}
339
340/// Configuration for a peer pool
341#[derive(Debug, Clone, Copy)]
342pub struct PoolConfig {
343    /// Maximum connections in this pool
344    pub max_connections: usize,
345    /// Number of connections to consider "satisfied" (stop sending hellos)
346    pub satisfied_connections: usize,
347}
348
349impl Default for PoolConfig {
350    fn default() -> Self {
351        Self {
352            max_connections: 16,
353            satisfied_connections: 8,
354        }
355    }
356}
357
358/// Pool settings for both pools
359#[derive(Debug, Clone)]
360pub struct PoolSettings {
361    pub follows: PoolConfig,
362    pub other: PoolConfig,
363}
364
365impl Default for PoolSettings {
366    fn default() -> Self {
367        Self {
368            follows: PoolConfig {
369                max_connections: 16,
370                satisfied_connections: 8,
371            },
372            other: PoolConfig {
373                max_connections: 16,
374                satisfied_connections: 8,
375            },
376        }
377    }
378}
379
380impl std::fmt::Display for PeerDirection {
381    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
382        match self {
383            PeerDirection::Inbound => write!(f, "inbound"),
384            PeerDirection::Outbound => write!(f, "outbound"),
385        }
386    }
387}
388
389/// Message type bytes (prefix before MessagePack body)
390pub const MSG_TYPE_REQUEST: u8 = 0x00;
391pub const MSG_TYPE_RESPONSE: u8 = 0x01;
392pub const MSG_TYPE_QUOTE_REQUEST: u8 = 0x02;
393pub const MSG_TYPE_QUOTE_RESPONSE: u8 = 0x03;
394pub const MSG_TYPE_PAYMENT: u8 = 0x04;
395pub const MSG_TYPE_PAYMENT_ACK: u8 = 0x05;
396pub const MSG_TYPE_CHUNK: u8 = 0x06;
397
398/// Hashtree data channel protocol messages
399/// Shared between WebRTC data channels and WebSocket transport
400///
401/// Wire format: [type byte][msgpack body]
402/// Request:  [0x00][msgpack: {h: bytes32, htl?: u8, q?: u64}]
403/// Response: [0x01][msgpack: {h: bytes32, d: bytes}]
404/// QuoteRequest:  [0x02][msgpack: {h: bytes32, p: u64, t: u32, m?: string}]
405/// QuoteResponse: [0x03][msgpack: {h: bytes32, a: bool, q?: u64, p?: u64, t?: u32, m?: string}]
406/// Payment:       [0x04][msgpack: {h: bytes32, q: u64, c: u32, p: u64, m?: string, tok: string}]
407/// PaymentAck:    [0x05][msgpack: {h: bytes32, q: u64, c: u32, a: bool, e?: string}]
408/// Chunk:         [0x06][msgpack: {h: bytes32, q: u64, c: u32, n: u32, p: u64, d: bytes}]
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct DataRequest {
412    #[serde(with = "serde_bytes")]
413    pub h: Vec<u8>, // 32-byte hash
414    #[serde(default = "default_htl", skip_serializing_if = "is_max_htl")]
415    pub htl: u8,
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub q: Option<u64>,
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct DataResponse {
422    #[serde(with = "serde_bytes")]
423    pub h: Vec<u8>, // 32-byte hash
424    #[serde(with = "serde_bytes")]
425    pub d: Vec<u8>, // Data
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct DataQuoteRequest {
430    #[serde(with = "serde_bytes")]
431    pub h: Vec<u8>,
432    pub p: u64,
433    pub t: u32,
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub m: Option<String>,
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct DataQuoteResponse {
440    #[serde(with = "serde_bytes")]
441    pub h: Vec<u8>,
442    pub a: bool,
443    #[serde(skip_serializing_if = "Option::is_none")]
444    pub q: Option<u64>,
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub p: Option<u64>,
447    #[serde(skip_serializing_if = "Option::is_none")]
448    pub t: Option<u32>,
449    #[serde(skip_serializing_if = "Option::is_none")]
450    pub m: Option<String>,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
454pub struct DataPayment {
455    #[serde(with = "serde_bytes")]
456    pub h: Vec<u8>,
457    pub q: u64,
458    pub c: u32,
459    pub p: u64,
460    #[serde(skip_serializing_if = "Option::is_none")]
461    pub m: Option<String>,
462    pub tok: String,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct DataPaymentAck {
467    #[serde(with = "serde_bytes")]
468    pub h: Vec<u8>,
469    pub q: u64,
470    pub c: u32,
471    pub a: bool,
472    #[serde(skip_serializing_if = "Option::is_none")]
473    pub e: Option<String>,
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize)]
477pub struct DataChunk {
478    #[serde(with = "serde_bytes")]
479    pub h: Vec<u8>,
480    pub q: u64,
481    pub c: u32,
482    pub n: u32,
483    pub p: u64,
484    #[serde(with = "serde_bytes")]
485    pub d: Vec<u8>,
486}
487
488#[derive(Debug, Clone)]
489pub enum DataMessage {
490    Request(DataRequest),
491    Response(DataResponse),
492    QuoteRequest(DataQuoteRequest),
493    QuoteResponse(DataQuoteResponse),
494    Payment(DataPayment),
495    PaymentAck(DataPaymentAck),
496    Chunk(DataChunk),
497}
498
499fn default_htl() -> u8 {
500    BLOB_REQUEST_POLICY.max_htl
501}
502
503fn is_max_htl(htl: &u8) -> bool {
504    *htl == BLOB_REQUEST_POLICY.max_htl
505}
506
507/// Encode a request to wire format: [0x00][msgpack body]
508/// Uses named fields for cross-language compatibility with TypeScript
509pub fn encode_request(req: &DataRequest) -> Result<Vec<u8>, rmp_serde::encode::Error> {
510    let body = rmp_serde::to_vec_named(req)?;
511    let mut result = Vec::with_capacity(1 + body.len());
512    result.push(MSG_TYPE_REQUEST);
513    result.extend(body);
514    Ok(result)
515}
516
517/// Encode a response to wire format: [0x01][msgpack body]
518/// Uses named fields for cross-language compatibility with TypeScript
519pub fn encode_response(res: &DataResponse) -> Result<Vec<u8>, rmp_serde::encode::Error> {
520    let body = rmp_serde::to_vec_named(res)?;
521    let mut result = Vec::with_capacity(1 + body.len());
522    result.push(MSG_TYPE_RESPONSE);
523    result.extend(body);
524    Ok(result)
525}
526
527/// Encode a quote request to wire format: [0x02][msgpack body]
528pub fn encode_quote_request(req: &DataQuoteRequest) -> Result<Vec<u8>, rmp_serde::encode::Error> {
529    let body = rmp_serde::to_vec_named(req)?;
530    let mut result = Vec::with_capacity(1 + body.len());
531    result.push(MSG_TYPE_QUOTE_REQUEST);
532    result.extend(body);
533    Ok(result)
534}
535
536/// Encode a quote response to wire format: [0x03][msgpack body]
537pub fn encode_quote_response(res: &DataQuoteResponse) -> Result<Vec<u8>, rmp_serde::encode::Error> {
538    let body = rmp_serde::to_vec_named(res)?;
539    let mut result = Vec::with_capacity(1 + body.len());
540    result.push(MSG_TYPE_QUOTE_RESPONSE);
541    result.extend(body);
542    Ok(result)
543}
544
545pub fn encode_payment(req: &DataPayment) -> Result<Vec<u8>, rmp_serde::encode::Error> {
546    let body = rmp_serde::to_vec_named(req)?;
547    let mut result = Vec::with_capacity(1 + body.len());
548    result.push(MSG_TYPE_PAYMENT);
549    result.extend(body);
550    Ok(result)
551}
552
553pub fn encode_payment_ack(res: &DataPaymentAck) -> Result<Vec<u8>, rmp_serde::encode::Error> {
554    let body = rmp_serde::to_vec_named(res)?;
555    let mut result = Vec::with_capacity(1 + body.len());
556    result.push(MSG_TYPE_PAYMENT_ACK);
557    result.extend(body);
558    Ok(result)
559}
560
561pub fn encode_chunk(chunk: &DataChunk) -> Result<Vec<u8>, rmp_serde::encode::Error> {
562    let body = rmp_serde::to_vec_named(chunk)?;
563    let mut result = Vec::with_capacity(1 + body.len());
564    result.push(MSG_TYPE_CHUNK);
565    result.extend(body);
566    Ok(result)
567}
568
569/// Parse a wire format message
570pub fn parse_message(data: &[u8]) -> Result<DataMessage, rmp_serde::decode::Error> {
571    if data.is_empty() {
572        return Err(rmp_serde::decode::Error::LengthMismatch(0));
573    }
574
575    let msg_type = data[0];
576    let body = &data[1..];
577
578    match msg_type {
579        MSG_TYPE_REQUEST => {
580            let req: DataRequest = rmp_serde::from_slice(body)?;
581            Ok(DataMessage::Request(req))
582        }
583        MSG_TYPE_RESPONSE => {
584            let res: DataResponse = rmp_serde::from_slice(body)?;
585            Ok(DataMessage::Response(res))
586        }
587        MSG_TYPE_QUOTE_REQUEST => {
588            let req: DataQuoteRequest = rmp_serde::from_slice(body)?;
589            Ok(DataMessage::QuoteRequest(req))
590        }
591        MSG_TYPE_QUOTE_RESPONSE => {
592            let res: DataQuoteResponse = rmp_serde::from_slice(body)?;
593            Ok(DataMessage::QuoteResponse(res))
594        }
595        MSG_TYPE_PAYMENT => {
596            let req: DataPayment = rmp_serde::from_slice(body)?;
597            Ok(DataMessage::Payment(req))
598        }
599        MSG_TYPE_PAYMENT_ACK => {
600            let res: DataPaymentAck = rmp_serde::from_slice(body)?;
601            Ok(DataMessage::PaymentAck(res))
602        }
603        MSG_TYPE_CHUNK => {
604            let chunk: DataChunk = rmp_serde::from_slice(body)?;
605            Ok(DataMessage::Chunk(chunk))
606        }
607        _ => Err(rmp_serde::decode::Error::LengthMismatch(msg_type as u32)),
608    }
609}
610
611/// Convert hash to hex string for logging/map keys
612pub fn hash_to_hex(hash: &[u8]) -> String {
613    hash.iter().map(|b| format!("{:02x}", b)).collect()
614}
615
616/// Encode a DataMessage to wire format (deprecated - use encode_request/encode_response)
617pub fn encode_message(msg: &DataMessage) -> Result<Vec<u8>, rmp_serde::encode::Error> {
618    match msg {
619        DataMessage::Request(req) => encode_request(req),
620        DataMessage::Response(res) => encode_response(res),
621        DataMessage::QuoteRequest(req) => encode_quote_request(req),
622        DataMessage::QuoteResponse(res) => encode_quote_response(res),
623        DataMessage::Payment(req) => encode_payment(req),
624        DataMessage::PaymentAck(res) => encode_payment_ack(res),
625        DataMessage::Chunk(chunk) => encode_chunk(chunk),
626    }
627}