Skip to main content

fips_core/transport/
handle.rs

1//! Enum wrapper for concrete transport implementations.
2
3#[cfg(target_os = "linux")]
4use super::ble::DefaultBleTransport;
5#[cfg(any(target_os = "linux", target_os = "macos"))]
6use super::ethernet::EthernetTransport;
7#[cfg(feature = "sim-transport")]
8use super::sim::SimTransport;
9use super::tcp::TcpTransport;
10use super::tor::TorTransport;
11use super::tor::control::TorMonitoringInfo;
12use super::udp::UdpTransport;
13#[cfg(feature = "webrtc-transport")]
14use super::webrtc::WebRtcTransport;
15use super::websocket::WebSocketTransport;
16use super::{
17    ConnectionState, DiscoveredPeer, Transport, TransportAddr, TransportCongestion, TransportError,
18    TransportId, TransportState, TransportType,
19};
20
21/// Wrapper enum for concrete transport implementations.
22///
23/// This enables polymorphic transport handling without trait objects,
24/// supporting async methods that the sync Transport trait cannot express.
25pub enum TransportHandle {
26    /// UDP/IP transport.
27    Udp(UdpTransport),
28    /// In-memory simulated packet transport.
29    #[cfg(feature = "sim-transport")]
30    Sim(SimTransport),
31    /// Raw Ethernet transport.
32    #[cfg(any(target_os = "linux", target_os = "macos"))]
33    Ethernet(EthernetTransport),
34    /// TCP/IP transport.
35    Tcp(TcpTransport),
36    /// Binary WebSocket physical transport.
37    WebSocket(Box<WebSocketTransport>),
38    /// Tor transport (via SOCKS5).
39    Tor(TorTransport),
40    /// WebRTC DataChannel transport.
41    #[cfg(feature = "webrtc-transport")]
42    WebRtc(Box<WebRtcTransport>),
43    /// BLE L2CAP transport.
44    #[cfg(target_os = "linux")]
45    Ble(DefaultBleTransport),
46}
47
48impl TransportHandle {
49    /// Normalize transport-specific addresses before Node stores or compares
50    /// them. Most transports have one textual identity; WebRTC uses Nostr's
51    /// x-only identity and therefore collapses legacy 02/03 compressed forms.
52    pub(crate) fn canonical_addr(
53        &self,
54        addr: &TransportAddr,
55    ) -> Result<TransportAddr, TransportError> {
56        match self {
57            #[cfg(feature = "webrtc-transport")]
58            TransportHandle::WebRtc(_) => super::webrtc::canonical_webrtc_addr(addr),
59            _ => Ok(addr.clone()),
60        }
61    }
62
63    /// Drain adapter negotiations that must travel over the standard FSP
64    /// link-negotiation service.
65    #[cfg(feature = "webrtc-transport")]
66    pub(crate) fn drain_link_negotiations(
67        &mut self,
68        limit: usize,
69    ) -> Vec<super::link_negotiation::OutboundLinkNegotiation> {
70        match self {
71            TransportHandle::WebRtc(transport) => transport.drain_link_negotiations(limit),
72            _ => Vec::new(),
73        }
74    }
75
76    /// Deliver an authenticated negotiation to the enabled matching adapter.
77    #[cfg(feature = "webrtc-transport")]
78    pub(crate) fn ingest_link_negotiation(
79        &self,
80        source: secp256k1::PublicKey,
81        message: super::link_negotiation::LinkNegotiationMessage,
82    ) -> Result<(), TransportError> {
83        match self {
84            TransportHandle::WebRtc(transport) if message.link_type == "webrtc" => {
85                transport.ingest_link_negotiation(source, message)
86            }
87            _ => Err(TransportError::NotSupported(
88                "no enabled adapter accepts this link negotiation".into(),
89            )),
90        }
91    }
92
93    /// Start the transport asynchronously.
94    pub async fn start(&mut self) -> Result<(), TransportError> {
95        match self {
96            TransportHandle::Udp(t) => t.start_async().await,
97            #[cfg(feature = "sim-transport")]
98            TransportHandle::Sim(t) => t.start_async().await,
99            #[cfg(any(target_os = "linux", target_os = "macos"))]
100            TransportHandle::Ethernet(t) => t.start_async().await,
101            TransportHandle::Tcp(t) => t.start_async().await,
102            TransportHandle::WebSocket(t) => t.start_async().await,
103            TransportHandle::Tor(t) => t.start_async().await,
104            #[cfg(feature = "webrtc-transport")]
105            TransportHandle::WebRtc(t) => t.start_async().await,
106            #[cfg(target_os = "linux")]
107            TransportHandle::Ble(t) => t.start_async().await,
108        }
109    }
110
111    /// Stop the transport asynchronously.
112    pub async fn stop(&mut self) -> Result<(), TransportError> {
113        match self {
114            TransportHandle::Udp(t) => t.stop_async().await,
115            #[cfg(feature = "sim-transport")]
116            TransportHandle::Sim(t) => t.stop_async().await,
117            #[cfg(any(target_os = "linux", target_os = "macos"))]
118            TransportHandle::Ethernet(t) => t.stop_async().await,
119            TransportHandle::Tcp(t) => t.stop_async().await,
120            TransportHandle::WebSocket(t) => t.stop_async().await,
121            TransportHandle::Tor(t) => t.stop_async().await,
122            #[cfg(feature = "webrtc-transport")]
123            TransportHandle::WebRtc(t) => t.stop_async().await,
124            #[cfg(target_os = "linux")]
125            TransportHandle::Ble(t) => t.stop_async().await,
126        }
127    }
128
129    /// Send data to a remote address asynchronously.
130    pub async fn send(&self, addr: &TransportAddr, data: &[u8]) -> Result<usize, TransportError> {
131        match self {
132            TransportHandle::Udp(t) => t.send_async(addr, data).await,
133            #[cfg(feature = "sim-transport")]
134            TransportHandle::Sim(t) => t.send_async(addr, data).await,
135            #[cfg(any(target_os = "linux", target_os = "macos"))]
136            TransportHandle::Ethernet(t) => t.send_async(addr, data).await,
137            TransportHandle::Tcp(t) => t.send_async(addr, data).await,
138            TransportHandle::WebSocket(t) => t.send_async(addr, data).await,
139            TransportHandle::Tor(t) => t.send_async(addr, data).await,
140            #[cfg(feature = "webrtc-transport")]
141            // Keep this optional adapter's large future out of every transport send frame.
142            TransportHandle::WebRtc(t) => Box::pin(t.send_async(addr, data)).await,
143            #[cfg(target_os = "linux")]
144            TransportHandle::Ble(t) => t.send_async(addr, data).await,
145        }
146    }
147
148    pub(crate) async fn send_with_timeout(
149        &self,
150        addr: &TransportAddr,
151        data: &[u8],
152        timeout: std::time::Duration,
153    ) -> Result<usize, TransportError> {
154        match self {
155            TransportHandle::Tcp(transport) => {
156                transport.send_async_with_timeout(addr, data, timeout).await
157            }
158            _ => self.send(addr, data).await,
159        }
160    }
161
162    /// Get the transport ID.
163    pub fn transport_id(&self) -> TransportId {
164        match self {
165            TransportHandle::Udp(t) => t.transport_id(),
166            #[cfg(feature = "sim-transport")]
167            TransportHandle::Sim(t) => t.transport_id(),
168            #[cfg(any(target_os = "linux", target_os = "macos"))]
169            TransportHandle::Ethernet(t) => t.transport_id(),
170            TransportHandle::Tcp(t) => t.transport_id(),
171            TransportHandle::WebSocket(t) => t.transport_id(),
172            TransportHandle::Tor(t) => t.transport_id(),
173            #[cfg(feature = "webrtc-transport")]
174            TransportHandle::WebRtc(t) => t.transport_id(),
175            #[cfg(target_os = "linux")]
176            TransportHandle::Ble(t) => t.transport_id(),
177        }
178    }
179
180    /// Whether this carrier uses the shared FMP/FSP byte-stream record reader.
181    pub(crate) fn uses_fips_byte_stream_framing(&self) -> bool {
182        matches!(self, TransportHandle::Tcp(_) | TransportHandle::Tor(_))
183    }
184
185    /// Get the instance name (if configured as a named instance).
186    pub fn name(&self) -> Option<&str> {
187        match self {
188            TransportHandle::Udp(t) => t.name(),
189            #[cfg(feature = "sim-transport")]
190            TransportHandle::Sim(t) => t.name(),
191            #[cfg(any(target_os = "linux", target_os = "macos"))]
192            TransportHandle::Ethernet(t) => t.name(),
193            TransportHandle::Tcp(t) => t.name(),
194            TransportHandle::WebSocket(t) => t.name(),
195            TransportHandle::Tor(t) => t.name(),
196            #[cfg(feature = "webrtc-transport")]
197            TransportHandle::WebRtc(t) => t.name(),
198            #[cfg(target_os = "linux")]
199            TransportHandle::Ble(t) => t.name(),
200        }
201    }
202
203    /// Get the transport type metadata.
204    pub fn transport_type(&self) -> &TransportType {
205        match self {
206            TransportHandle::Udp(t) => t.transport_type(),
207            #[cfg(feature = "sim-transport")]
208            TransportHandle::Sim(t) => t.transport_type(),
209            #[cfg(any(target_os = "linux", target_os = "macos"))]
210            TransportHandle::Ethernet(t) => t.transport_type(),
211            TransportHandle::Tcp(t) => t.transport_type(),
212            TransportHandle::WebSocket(t) => t.transport_type(),
213            TransportHandle::Tor(t) => t.transport_type(),
214            #[cfg(feature = "webrtc-transport")]
215            TransportHandle::WebRtc(t) => t.transport_type(),
216            #[cfg(target_os = "linux")]
217            TransportHandle::Ble(t) => t.transport_type(),
218        }
219    }
220
221    /// Get current transport state.
222    pub fn state(&self) -> TransportState {
223        match self {
224            TransportHandle::Udp(t) => t.state(),
225            #[cfg(feature = "sim-transport")]
226            TransportHandle::Sim(t) => t.state(),
227            #[cfg(any(target_os = "linux", target_os = "macos"))]
228            TransportHandle::Ethernet(t) => t.state(),
229            TransportHandle::Tcp(t) => t.state(),
230            TransportHandle::WebSocket(t) => t.state(),
231            TransportHandle::Tor(t) => t.state(),
232            #[cfg(feature = "webrtc-transport")]
233            TransportHandle::WebRtc(t) => t.state(),
234            #[cfg(target_os = "linux")]
235            TransportHandle::Ble(t) => t.state(),
236        }
237    }
238
239    /// Get the transport MTU.
240    pub fn mtu(&self) -> u16 {
241        match self {
242            TransportHandle::Udp(t) => t.mtu(),
243            #[cfg(feature = "sim-transport")]
244            TransportHandle::Sim(t) => t.mtu(),
245            #[cfg(any(target_os = "linux", target_os = "macos"))]
246            TransportHandle::Ethernet(t) => t.mtu(),
247            TransportHandle::Tcp(t) => t.mtu(),
248            TransportHandle::WebSocket(t) => t.mtu(),
249            TransportHandle::Tor(t) => t.mtu(),
250            #[cfg(feature = "webrtc-transport")]
251            TransportHandle::WebRtc(t) => t.mtu(),
252            #[cfg(target_os = "linux")]
253            TransportHandle::Ble(t) => t.mtu(),
254        }
255    }
256
257    /// Get the MTU for a specific link address.
258    ///
259    /// Falls back to transport-wide MTU if the transport doesn't
260    /// support per-link MTU or the address is unknown.
261    pub fn link_mtu(&self, addr: &TransportAddr) -> u16 {
262        match self {
263            TransportHandle::Udp(t) => t.link_mtu(addr),
264            #[cfg(feature = "sim-transport")]
265            TransportHandle::Sim(t) => t.link_mtu(addr),
266            #[cfg(any(target_os = "linux", target_os = "macos"))]
267            TransportHandle::Ethernet(t) => t.link_mtu(addr),
268            TransportHandle::Tcp(t) => t.link_mtu(addr),
269            TransportHandle::WebSocket(t) => t.link_mtu(addr),
270            TransportHandle::Tor(t) => t.link_mtu(addr),
271            #[cfg(feature = "webrtc-transport")]
272            TransportHandle::WebRtc(t) => t.link_mtu(addr),
273            #[cfg(target_os = "linux")]
274            TransportHandle::Ble(t) => t.link_mtu(addr),
275        }
276    }
277
278    /// Get the local bound address (UDP/TCP only, returns None for other transports).
279    pub fn local_addr(&self) -> Option<std::net::SocketAddr> {
280        match self {
281            TransportHandle::Udp(t) => t.local_addr(),
282            #[cfg(feature = "sim-transport")]
283            TransportHandle::Sim(_) => None,
284            #[cfg(any(target_os = "linux", target_os = "macos"))]
285            TransportHandle::Ethernet(_) => None,
286            TransportHandle::Tcp(t) => t.local_addr(),
287            TransportHandle::WebSocket(t) => t.local_addr(),
288            TransportHandle::Tor(_) => None,
289            #[cfg(feature = "webrtc-transport")]
290            TransportHandle::WebRtc(_) => None,
291            #[cfg(target_os = "linux")]
292            TransportHandle::Ble(_) => None,
293        }
294    }
295
296    /// Resolve a UDP target only if doing so cannot block on DNS.
297    ///
298    /// Numeric UDP addresses are returned directly; hostnames are returned
299    /// only when the UDP transport already has a fresh cached resolution.
300    pub(crate) fn resolved_udp_socket_addr_if_cached(
301        &self,
302        addr: &TransportAddr,
303    ) -> Option<std::net::SocketAddr> {
304        match self {
305            TransportHandle::Udp(t) => t.resolved_socket_addr_if_cached(addr),
306            _ => None,
307        }
308    }
309
310    /// Get the interface name (Ethernet only, returns None for other transports).
311    pub fn interface_name(&self) -> Option<&str> {
312        match self {
313            TransportHandle::Udp(_) => None,
314            #[cfg(feature = "sim-transport")]
315            TransportHandle::Sim(_) => None,
316            #[cfg(any(target_os = "linux", target_os = "macos"))]
317            TransportHandle::Ethernet(t) => Some(t.interface_name()),
318            TransportHandle::Tcp(_) => None,
319            TransportHandle::WebSocket(_) => None,
320            TransportHandle::Tor(_) => None,
321            #[cfg(feature = "webrtc-transport")]
322            TransportHandle::WebRtc(_) => None,
323            #[cfg(target_os = "linux")]
324            TransportHandle::Ble(_) => None,
325        }
326    }
327
328    /// Get the onion service address (Tor only, returns None for other transports).
329    pub fn onion_address(&self) -> Option<&str> {
330        match self {
331            TransportHandle::Tor(t) => t.onion_address(),
332            _ => None,
333        }
334    }
335
336    /// Get cached Tor daemon monitoring info (Tor only).
337    pub fn tor_monitoring(&self) -> Option<TorMonitoringInfo> {
338        match self {
339            TransportHandle::Tor(t) => t.cached_monitoring(),
340            _ => None,
341        }
342    }
343
344    /// Get the Tor transport mode (Tor only).
345    pub fn tor_mode(&self) -> Option<&str> {
346        match self {
347            TransportHandle::Tor(t) => Some(t.mode()),
348            _ => None,
349        }
350    }
351
352    /// Drain discovered peers from this transport.
353    pub fn discover(&self) -> Result<Vec<DiscoveredPeer>, TransportError> {
354        match self {
355            TransportHandle::Udp(t) => t.discover(),
356            #[cfg(feature = "sim-transport")]
357            TransportHandle::Sim(t) => t.discover(),
358            #[cfg(any(target_os = "linux", target_os = "macos"))]
359            TransportHandle::Ethernet(t) => t.discover(),
360            TransportHandle::Tcp(t) => t.discover(),
361            TransportHandle::WebSocket(t) => t.discover(),
362            TransportHandle::Tor(t) => t.discover(),
363            #[cfg(feature = "webrtc-transport")]
364            TransportHandle::WebRtc(t) => t.discover(),
365            #[cfg(target_os = "linux")]
366            TransportHandle::Ble(t) => t.discover(),
367        }
368    }
369
370    /// Whether this transport auto-connects to discovered peers.
371    pub fn auto_connect(&self) -> bool {
372        match self {
373            TransportHandle::Udp(t) => t.auto_connect(),
374            #[cfg(feature = "sim-transport")]
375            TransportHandle::Sim(t) => t.auto_connect(),
376            #[cfg(any(target_os = "linux", target_os = "macos"))]
377            TransportHandle::Ethernet(t) => t.auto_connect(),
378            TransportHandle::Tcp(t) => t.auto_connect(),
379            TransportHandle::WebSocket(t) => t.auto_connect(),
380            TransportHandle::Tor(t) => t.auto_connect(),
381            #[cfg(feature = "webrtc-transport")]
382            TransportHandle::WebRtc(t) => t.auto_connect(),
383            #[cfg(target_os = "linux")]
384            TransportHandle::Ble(t) => t.auto_connect(),
385        }
386    }
387
388    /// Whether this transport accepts inbound connections.
389    pub fn accept_connections(&self) -> bool {
390        match self {
391            TransportHandle::Udp(t) => t.accept_connections(),
392            #[cfg(feature = "sim-transport")]
393            TransportHandle::Sim(t) => t.accept_connections(),
394            #[cfg(any(target_os = "linux", target_os = "macos"))]
395            TransportHandle::Ethernet(t) => t.accept_connections(),
396            TransportHandle::Tcp(t) => t.accept_connections(),
397            TransportHandle::WebSocket(t) => t.accept_connections(),
398            TransportHandle::Tor(t) => t.accept_connections(),
399            #[cfg(feature = "webrtc-transport")]
400            TransportHandle::WebRtc(t) => t.accept_connections(),
401            #[cfg(target_os = "linux")]
402            TransportHandle::Ble(t) => t.accept_connections(),
403        }
404    }
405
406    /// Initiate a non-blocking connection to a remote address.
407    ///
408    /// For connection-oriented transports (TCP, Tor), spawns a background
409    /// task to establish the connection. For connectionless transports
410    /// (UDP, Ethernet), this is a no-op that returns Ok immediately.
411    ///
412    /// Poll `connection_state()` to check when the connection is ready.
413    pub async fn connect(&self, addr: &TransportAddr) -> Result<(), TransportError> {
414        match self {
415            TransportHandle::Udp(_) => Ok(()), // connectionless
416            #[cfg(feature = "sim-transport")]
417            TransportHandle::Sim(_) => Ok(()), // connectionless
418            #[cfg(any(target_os = "linux", target_os = "macos"))]
419            TransportHandle::Ethernet(_) => Ok(()), // connectionless
420            TransportHandle::Tcp(t) => t.connect_async(addr).await,
421            TransportHandle::WebSocket(t) => t.connect_async(addr).await,
422            TransportHandle::Tor(t) => t.connect_async(addr).await,
423            #[cfg(feature = "webrtc-transport")]
424            TransportHandle::WebRtc(t) => t.connect_async(addr).await,
425            #[cfg(target_os = "linux")]
426            TransportHandle::Ble(t) => t.connect_async(addr).await,
427        }
428    }
429
430    /// Query the state of a connection attempt to a remote address.
431    ///
432    /// For connectionless transports, always returns `ConnectionState::Connected`
433    /// (they are always "connected"). For connection-oriented transports, returns
434    /// the current state of the background connection attempt.
435    pub fn connection_state(&self, addr: &TransportAddr) -> ConnectionState {
436        match self {
437            TransportHandle::Udp(_) => ConnectionState::Connected,
438            #[cfg(feature = "sim-transport")]
439            TransportHandle::Sim(_) => ConnectionState::Connected,
440            #[cfg(any(target_os = "linux", target_os = "macos"))]
441            TransportHandle::Ethernet(_) => ConnectionState::Connected,
442            TransportHandle::Tcp(t) => t.connection_state_sync(addr),
443            TransportHandle::WebSocket(t) => t.connection_state_sync(addr),
444            TransportHandle::Tor(t) => t.connection_state_sync(addr),
445            #[cfg(feature = "webrtc-transport")]
446            TransportHandle::WebRtc(t) => t.connection_state_sync(addr),
447            #[cfg(target_os = "linux")]
448            TransportHandle::Ble(t) => t.connection_state_sync(addr),
449        }
450    }
451
452    /// Close a specific connection on this transport.
453    ///
454    /// No-op for connectionless transports. For TCP/Tor, removes the
455    /// connection from the pool and drops the stream.
456    pub async fn close_connection(&self, addr: &TransportAddr) {
457        match self {
458            TransportHandle::Udp(t) => t.close_connection(addr),
459            #[cfg(feature = "sim-transport")]
460            TransportHandle::Sim(t) => t.close_connection(addr),
461            #[cfg(any(target_os = "linux", target_os = "macos"))]
462            TransportHandle::Ethernet(t) => t.close_connection(addr),
463            TransportHandle::Tcp(t) => t.close_connection_async(addr).await,
464            TransportHandle::WebSocket(t) => t.close_connection_async(addr).await,
465            TransportHandle::Tor(t) => t.close_connection_async(addr).await,
466            #[cfg(feature = "webrtc-transport")]
467            TransportHandle::WebRtc(t) => t.close_connection_async(addr).await,
468            #[cfg(target_os = "linux")]
469            TransportHandle::Ble(t) => t.close_connection_async(addr).await,
470        }
471    }
472
473    /// Schedule cleanup for a connection removed by a synchronous node path.
474    pub fn close_connection_detached(&self, _addr: &TransportAddr) {
475        #[cfg(feature = "webrtc-transport")]
476        if let TransportHandle::WebRtc(transport) = self {
477            transport.close_connection_detached(_addr);
478        }
479    }
480
481    /// Check if transport is operational.
482    pub fn is_operational(&self) -> bool {
483        self.state().is_operational()
484    }
485
486    /// Query transport-local congestion indicators.
487    ///
488    /// Returns a snapshot of congestion signals that the transport can
489    /// observe locally (e.g., kernel receive buffer drops). Fields are
490    /// `None` when the transport doesn't support that signal.
491    pub fn congestion(&self) -> TransportCongestion {
492        match self {
493            TransportHandle::Udp(t) => t.congestion(),
494            #[cfg(feature = "sim-transport")]
495            TransportHandle::Sim(_) => TransportCongestion::default(),
496            #[cfg(any(target_os = "linux", target_os = "macos"))]
497            TransportHandle::Ethernet(_) => TransportCongestion::default(),
498            TransportHandle::Tcp(_) => TransportCongestion::default(),
499            TransportHandle::WebSocket(_) => TransportCongestion::default(),
500            TransportHandle::Tor(_) => TransportCongestion::default(),
501            #[cfg(feature = "webrtc-transport")]
502            TransportHandle::WebRtc(_) => TransportCongestion::default(),
503            #[cfg(target_os = "linux")]
504            TransportHandle::Ble(_) => TransportCongestion::default(),
505        }
506    }
507
508    /// Get transport-specific stats as a JSON value.
509    ///
510    /// Returns a snapshot of counters for the specific transport type.
511    pub fn transport_stats(&self) -> serde_json::Value {
512        match self {
513            TransportHandle::Udp(t) => {
514                serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
515            }
516            #[cfg(feature = "sim-transport")]
517            TransportHandle::Sim(t) => serde_json::to_value(t.stats()).unwrap_or_default(),
518            #[cfg(any(target_os = "linux", target_os = "macos"))]
519            TransportHandle::Ethernet(t) => {
520                let snap = t.stats().snapshot();
521                serde_json::json!({
522                    "frames_sent": snap.frames_sent,
523                    "frames_recv": snap.frames_recv,
524                    "bytes_sent": snap.bytes_sent,
525                    "bytes_recv": snap.bytes_recv,
526                    "send_errors": snap.send_errors,
527                    "recv_errors": snap.recv_errors,
528                    "beacons_sent": snap.beacons_sent,
529                    "beacons_recv": snap.beacons_recv,
530                    "frames_too_short": snap.frames_too_short,
531                    "frames_too_long": snap.frames_too_long,
532                })
533            }
534            TransportHandle::Tcp(t) => {
535                serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
536            }
537            TransportHandle::WebSocket(t) => serde_json::to_value(t.stats()).unwrap_or_default(),
538            TransportHandle::Tor(t) => {
539                serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
540            }
541            #[cfg(feature = "webrtc-transport")]
542            TransportHandle::WebRtc(t) => serde_json::json!({
543                "mtu": t.mtu(),
544                "state": t.state().to_string(),
545            }),
546            #[cfg(target_os = "linux")]
547            TransportHandle::Ble(t) => {
548                serde_json::to_value(t.stats().snapshot()).unwrap_or_default()
549            }
550        }
551    }
552}