Skip to main content

hashtree_cli/webrtc/
types.rs

1//! WebRTC signaling types compatible with iris-client and hashtree-ts
2
3use hashtree_network::DataMessage as SharedDataMessage;
4pub use hashtree_network::{
5    decrement_htl_with_policy, should_forward_htl, validate_mesh_frame, DataQuoteRequest,
6    DataQuoteResponse, DataRequest, DataResponse, HtlMode, HtlPolicy, IceCandidate, MeshNostrFrame,
7    MeshNostrPayload, PeerHTLConfig, PeerId, PeerPool, PoolConfig, PoolSettings,
8    RequestDispatchConfig, SelectionStrategy, SignalingMessage, TimedSeenSet, BLOB_REQUEST_POLICY,
9    DECREMENT_AT_MAX_PROB, DECREMENT_AT_MIN_PROB, MAX_HTL, MESH_DEFAULT_HTL, MESH_EVENT_POLICY,
10    MESH_MAX_HTL, MESH_PROTOCOL, MESH_PROTOCOL_VERSION, MSG_TYPE_QUOTE_REQUEST,
11    MSG_TYPE_QUOTE_RESPONSE, MSG_TYPE_REQUEST, MSG_TYPE_RESPONSE,
12};
13use serde::{Deserialize, Serialize};
14
15/// Backward-compatible helper using blob-request policy.
16pub fn decrement_htl(htl: u8, config: &PeerHTLConfig) -> u8 {
17    decrement_htl_with_policy(htl, &BLOB_REQUEST_POLICY, config)
18}
19
20/// Backward-compatible helper for existing call sites.
21pub fn should_forward(htl: u8) -> bool {
22    should_forward_htl(htl)
23}
24
25/// Event kind for WebRTC signaling (ephemeral kind 25050)
26/// All signaling uses this kind - hellos use #l tag, directed use gift wrap
27pub const WEBRTC_KIND: u64 = 25050;
28
29/// Tag for hello messages (broadcast discovery)
30pub const HELLO_TAG: &str = "hello";
31
32/// Legacy tag for WebRTC signaling messages (kept for compatibility)
33pub const WEBRTC_TAG: &str = "webrtc";
34
35/// Configuration for WebRTC manager
36#[derive(Clone)]
37pub struct WebRTCConfig {
38    /// Nostr relays for signaling
39    pub relays: Vec<String>,
40    /// Whether negotiated WebRTC signaling should run at all.
41    pub signaling_enabled: bool,
42    /// Maximum outbound connections (legacy, use pools instead)
43    pub max_outbound: usize,
44    /// Maximum inbound connections (legacy, use pools instead)
45    pub max_inbound: usize,
46    /// Hello message interval in milliseconds
47    pub hello_interval_ms: u64,
48    /// Message timeout in milliseconds
49    pub message_timeout_ms: u64,
50    /// STUN servers for NAT traversal
51    pub stun_servers: Vec<String>,
52    /// Enable debug logging
53    pub debug: bool,
54    /// Optional LAN multicast transport for offline discovery + root lookup.
55    pub multicast: super::multicast::MulticastConfig,
56    /// Optional Android Wi-Fi Aware nearby discovery/signaling bus.
57    pub wifi_aware: super::wifi_aware::WifiAwareConfig,
58    /// Optional native Bluetooth peer transport.
59    pub bluetooth: super::bluetooth::BluetoothConfig,
60    /// Pool settings for follows and other peers
61    pub pools: PoolSettings,
62    /// Retrieval peer selection strategy (shared with simulation).
63    pub request_selection_strategy: SelectionStrategy,
64    /// Whether fairness constraints are enabled for retrieval peer selection.
65    pub request_fairness_enabled: bool,
66    /// Hedged request dispatch policy for retrieval (shared with simulation).
67    pub request_dispatch: RequestDispatchConfig,
68}
69
70impl Default for WebRTCConfig {
71    fn default() -> Self {
72        Self {
73            relays: vec![
74                "wss://relay.damus.io".to_string(),
75                "wss://relay.primal.net".to_string(),
76                "wss://temp.iris.to".to_string(),
77                "wss://relay.snort.social".to_string(),
78            ],
79            signaling_enabled: true,
80            max_outbound: 6,
81            max_inbound: 6,
82            hello_interval_ms: 3000,
83            message_timeout_ms: 15000,
84            stun_servers: vec![
85                "stun:stun.iris.to:3478".to_string(),
86                "stun:stun.l.google.com:19302".to_string(),
87                "stun:stun.cloudflare.com:3478".to_string(),
88            ],
89            debug: false,
90            multicast: super::multicast::MulticastConfig::default(),
91            wifi_aware: super::wifi_aware::WifiAwareConfig::default(),
92            bluetooth: super::bluetooth::BluetoothConfig::default(),
93            pools: PoolSettings::default(),
94            request_selection_strategy: SelectionStrategy::TitForTat,
95            request_fairness_enabled: true,
96            request_dispatch: RequestDispatchConfig {
97                initial_fanout: 2,
98                hedge_fanout: 1,
99                max_fanout: 8,
100                hedge_interval_ms: 120,
101            },
102        }
103    }
104}
105
106/// Peer connection status
107#[derive(Debug, Clone)]
108pub struct PeerStatus {
109    pub peer_id: String,
110    pub pubkey: String,
111    pub state: String,
112    pub direction: PeerDirection,
113    pub connected_at: Option<std::time::Instant>,
114    pub pool: PeerPool,
115}
116
117/// Direction of peer connection
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum PeerDirection {
120    Inbound,
121    Outbound,
122}
123
124/// Peer state change event for signaling layer notification
125#[derive(Debug, Clone)]
126pub enum PeerStateEvent {
127    /// Peer connection succeeded
128    Connected(PeerId),
129    /// Peer connection failed
130    Failed(PeerId),
131    /// Peer disconnected
132    Disconnected(PeerId),
133}
134
135impl std::fmt::Display for PeerDirection {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        match self {
138            PeerDirection::Inbound => write!(f, "inbound"),
139            PeerDirection::Outbound => write!(f, "outbound"),
140        }
141    }
142}
143
144pub const MSG_TYPE_PAYMENT: u8 = 0x04;
145pub const MSG_TYPE_PAYMENT_ACK: u8 = 0x05;
146pub const MSG_TYPE_CHUNK: u8 = 0x06;
147
148/// Hashtree data channel protocol messages
149/// Shared between WebRTC data channels and WebSocket transport
150///
151/// Wire format: [type byte][msgpack body]
152/// Request:  [0x00][msgpack: {h: bytes32, htl?: u8, q?: u64}]
153/// Response: [0x01][msgpack: {h: bytes32, d: bytes}]
154/// QuoteRequest:  [0x02][msgpack: {h: bytes32, p: u64, t: u32, m?: string}]
155/// QuoteResponse: [0x03][msgpack: {h: bytes32, a: bool, q?: u64, p?: u64, t?: u32, m?: string}]
156/// Payment:       [0x04][msgpack: {h: bytes32, q: u64, c: u32, p: u64, m?: string, tok: string}]
157/// PaymentAck:    [0x05][msgpack: {h: bytes32, q: u64, c: u32, a: bool, e?: string}]
158/// Chunk:         [0x06][msgpack: {h: bytes32, q: u64, c: u32, n: u32, p: u64, d: bytes}]
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct DataPayment {
162    #[serde(with = "serde_bytes")]
163    pub h: Vec<u8>,
164    pub q: u64,
165    pub c: u32,
166    pub p: u64,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub m: Option<String>,
169    pub tok: String,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct DataPaymentAck {
174    #[serde(with = "serde_bytes")]
175    pub h: Vec<u8>,
176    pub q: u64,
177    pub c: u32,
178    pub a: bool,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub e: Option<String>,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct DataChunk {
185    #[serde(with = "serde_bytes")]
186    pub h: Vec<u8>,
187    pub q: u64,
188    pub c: u32,
189    pub n: u32,
190    pub p: u64,
191    #[serde(with = "serde_bytes")]
192    pub d: Vec<u8>,
193}
194
195#[derive(Debug, Clone)]
196pub enum DataMessage {
197    Request(DataRequest),
198    Response(DataResponse),
199    QuoteRequest(DataQuoteRequest),
200    QuoteResponse(DataQuoteResponse),
201    Payment(DataPayment),
202    PaymentAck(DataPaymentAck),
203    Chunk(DataChunk),
204}
205
206/// Encode a request to wire format: [0x00][msgpack body]
207/// Uses named fields for cross-language compatibility with TypeScript
208pub fn encode_request(req: &DataRequest) -> Result<Vec<u8>, rmp_serde::encode::Error> {
209    Ok(hashtree_network::encode_request(req))
210}
211
212/// Encode a response to wire format: [0x01][msgpack body]
213/// Uses named fields for cross-language compatibility with TypeScript
214pub fn encode_response(res: &DataResponse) -> Result<Vec<u8>, rmp_serde::encode::Error> {
215    Ok(hashtree_network::encode_response(res))
216}
217
218/// Encode a quote request to wire format: [0x02][msgpack body]
219pub fn encode_quote_request(req: &DataQuoteRequest) -> Result<Vec<u8>, rmp_serde::encode::Error> {
220    Ok(hashtree_network::encode_quote_request(req))
221}
222
223/// Encode a quote response to wire format: [0x03][msgpack body]
224pub fn encode_quote_response(res: &DataQuoteResponse) -> Result<Vec<u8>, rmp_serde::encode::Error> {
225    Ok(hashtree_network::encode_quote_response(res))
226}
227
228pub fn encode_payment(req: &DataPayment) -> Result<Vec<u8>, rmp_serde::encode::Error> {
229    let body = rmp_serde::to_vec_named(req)?;
230    let mut result = Vec::with_capacity(1 + body.len());
231    result.push(MSG_TYPE_PAYMENT);
232    result.extend(body);
233    Ok(result)
234}
235
236pub fn encode_payment_ack(res: &DataPaymentAck) -> Result<Vec<u8>, rmp_serde::encode::Error> {
237    let body = rmp_serde::to_vec_named(res)?;
238    let mut result = Vec::with_capacity(1 + body.len());
239    result.push(MSG_TYPE_PAYMENT_ACK);
240    result.extend(body);
241    Ok(result)
242}
243
244pub fn encode_chunk(chunk: &DataChunk) -> Result<Vec<u8>, rmp_serde::encode::Error> {
245    let body = rmp_serde::to_vec_named(chunk)?;
246    let mut result = Vec::with_capacity(1 + body.len());
247    result.push(MSG_TYPE_CHUNK);
248    result.extend(body);
249    Ok(result)
250}
251
252/// Parse a wire format message
253pub fn parse_message(data: &[u8]) -> Result<DataMessage, rmp_serde::decode::Error> {
254    if data.is_empty() {
255        return Err(rmp_serde::decode::Error::LengthMismatch(0));
256    }
257
258    let msg_type = data[0];
259
260    match msg_type {
261        MSG_TYPE_REQUEST | MSG_TYPE_RESPONSE | MSG_TYPE_QUOTE_REQUEST | MSG_TYPE_QUOTE_RESPONSE => {
262            match hashtree_network::parse_message(data) {
263                Some(SharedDataMessage::Request(req)) => Ok(DataMessage::Request(req)),
264                Some(SharedDataMessage::Response(res)) => Ok(DataMessage::Response(res)),
265                Some(SharedDataMessage::QuoteRequest(req)) => Ok(DataMessage::QuoteRequest(req)),
266                Some(SharedDataMessage::QuoteResponse(res)) => Ok(DataMessage::QuoteResponse(res)),
267                None => Err(rmp_serde::decode::Error::LengthMismatch(msg_type as u32)),
268            }
269        }
270        MSG_TYPE_PAYMENT => {
271            let req: DataPayment = rmp_serde::from_slice(&data[1..])?;
272            Ok(DataMessage::Payment(req))
273        }
274        MSG_TYPE_PAYMENT_ACK => {
275            let res: DataPaymentAck = rmp_serde::from_slice(&data[1..])?;
276            Ok(DataMessage::PaymentAck(res))
277        }
278        MSG_TYPE_CHUNK => {
279            let chunk: DataChunk = rmp_serde::from_slice(&data[1..])?;
280            Ok(DataMessage::Chunk(chunk))
281        }
282        _ => Err(rmp_serde::decode::Error::LengthMismatch(msg_type as u32)),
283    }
284}
285
286/// Convert hash to hex string for logging/map keys
287pub fn hash_to_hex(hash: &[u8]) -> String {
288    hashtree_network::hash_to_key(hash)
289}
290
291/// Encode a DataMessage to wire format (deprecated - use encode_request/encode_response)
292pub fn encode_message(msg: &DataMessage) -> Result<Vec<u8>, rmp_serde::encode::Error> {
293    match msg {
294        DataMessage::Request(req) => encode_request(req),
295        DataMessage::Response(res) => encode_response(res),
296        DataMessage::QuoteRequest(req) => encode_quote_request(req),
297        DataMessage::QuoteResponse(res) => encode_quote_response(res),
298        DataMessage::Payment(req) => encode_payment(req),
299        DataMessage::PaymentAck(res) => encode_payment_ack(res),
300        DataMessage::Chunk(chunk) => encode_chunk(chunk),
301    }
302}