1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::net::SocketAddr;
4use std::pin::Pin;
5use std::str::FromStr;
6use std::sync::{Arc, Mutex};
7use std::time::Duration;
8
9use async_trait::async_trait;
10use fedimint_core::config::ALEPH_BFT_UNIT_BYTE_LIMIT;
11use fedimint_core::envs::{
12 FM_GW_IROH_CONNECT_OVERRIDES_PLAIN_ENV, FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV,
13 FM_IROH_N0_DISCOVERY_ENABLE_ENV, FM_IROH_PKARR_RESOLVER_ENABLE_ENV, is_env_var_set_opt,
14 parse_kv_list_from_env,
15};
16use fedimint_core::module::{
17 ApiError, ApiMethod, ApiRequestErased, FEDIMINT_API_ALPN, FEDIMINT_GATEWAY_ALPN,
18 IrohApiRequest, IrohGatewayRequest, IrohGatewayResponse,
19};
20use fedimint_core::net::iroh::{IROH_IDLE_TIMEOUT, IROH_KEEP_ALIVE_INTERVAL};
21
22const IROH_MAX_RESPONSE_BYTES: usize = ALEPH_BFT_UNIT_BYTE_LIMIT * 3600 * 4 * 2;
31
32const IROH_REQUEST_TIMEOUT_DEFAULT: Duration = Duration::from_secs(60);
39
40const IROH_REQUEST_TIMEOUT_LONG_POLL: Duration = Duration::from_secs(60 * 60);
47
48const IROH_REQUEST_TIMEOUT_ERROR_CODE: u32 = 1;
54const IROH_REQUEST_TIMEOUT_ERROR_REASON: &[u8] = b"request timeout";
55
56fn request_timeout_for_method(method: &ApiMethod) -> Duration {
66 let name = match method {
67 ApiMethod::Core(name) => name.as_str(),
68 ApiMethod::Module(_, name) => name.as_str(),
69 };
70 if name.starts_with("await_") || name.starts_with("wait_") {
71 IROH_REQUEST_TIMEOUT_LONG_POLL
72 } else {
73 IROH_REQUEST_TIMEOUT_DEFAULT
74 }
75}
76use fedimint_core::task::spawn;
77use fedimint_core::util::{FmtCompact as _, SafeUrl};
78use fedimint_core::{apply, async_trait_maybe_send};
79use fedimint_logging::LOG_NET_IROH;
80use futures::Future;
81use futures::stream::{FuturesUnordered, StreamExt};
82use iroh::discovery::pkarr::PkarrResolver;
83use iroh::endpoint::Connection;
84use iroh::{Endpoint, NodeAddr, NodeId, PublicKey};
85use reqwest::{Method, StatusCode};
86use serde_json::Value;
87use tokio::sync::watch;
88use tracing::{debug, trace, warn};
89
90use super::{DynGuaridianConnection, IGuardianConnection, ServerError, ServerResult};
91use crate::error::ConnectorError;
92use crate::{Connectivity, DynGatewayConnection, IConnection, IGatewayConnection, IrohPeerInfo};
93
94#[derive(Clone)]
95pub(crate) struct IrohConnector {
96 stable: iroh::endpoint::Endpoint,
97 next: iroh_next::endpoint::Endpoint,
98
99 connection_overrides: BTreeMap<NodeId, NodeAddr>,
105
106 path_change: Arc<watch::Sender<u64>>,
111
112 next_connections: Arc<Mutex<BTreeMap<NodeId, iroh_next::endpoint::Connection>>>,
121}
122
123impl fmt::Debug for IrohConnector {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 f.debug_struct("IrohEndpoint")
126 .field("stable-id", &self.stable.node_id())
127 .field("next-id", &self.next.id())
128 .finish_non_exhaustive()
129 }
130}
131
132impl IrohConnector {
133 pub(crate) async fn new(
134 iroh_dns: Option<SafeUrl>,
135 iroh_enable_dht: bool,
136 path_change: Arc<watch::Sender<u64>>,
137 ) -> Result<Self, ConnectorError> {
138 let mut s = Self::new_no_overrides(iroh_dns, iroh_enable_dht, path_change).await?;
139
140 for env_var in [
147 FM_IROH_CONNECT_OVERRIDES_PLAIN_ENV,
148 FM_GW_IROH_CONNECT_OVERRIDES_PLAIN_ENV,
149 ] {
150 for (k, v) in parse_kv_list_from_env::<NodeId, SocketAddr>(env_var) {
151 s = s.with_connection_override(k, NodeAddr::new(k).with_direct_addresses([v]));
152 }
153 }
154
155 Ok(s)
156 }
157
158 #[allow(clippy::too_many_lines)]
159 pub(crate) async fn new_no_overrides(
160 iroh_dns: Option<SafeUrl>,
161 iroh_enable_dht: bool,
162 path_change: Arc<watch::Sender<u64>>,
163 ) -> Result<Self, ConnectorError> {
164 let endpoint_stable = Box::pin({
165 let iroh_dns = iroh_dns.clone();
166 async {
167 let mut builder = Endpoint::builder();
168
169 if let Some(iroh_dns) = iroh_dns.map(SafeUrl::to_unsafe) {
170 builder = builder.add_discovery(|_| Some(PkarrResolver::new(iroh_dns)));
171 }
172
173 let mut builder = builder.relay_mode(iroh::RelayMode::Disabled);
175
176 #[cfg(not(target_family = "wasm"))]
177 if iroh_enable_dht {
178 builder = builder.discovery_dht();
179 }
180
181 {
184 if is_env_var_set_opt(FM_IROH_PKARR_RESOLVER_ENABLE_ENV).unwrap_or(true) {
185 builder = builder.add_discovery(move |_| Some(PkarrResolver::n0_dns()));
186 } else {
187 warn!(
188 target: LOG_NET_IROH,
189 "Iroh pkarr resolver is disabled"
190 );
191 }
192
193 if is_env_var_set_opt(FM_IROH_N0_DISCOVERY_ENABLE_ENV).unwrap_or(true) {
194 #[cfg(not(target_family = "wasm"))]
195 {
196 builder = builder.add_discovery(move |_| {
197 Some(iroh::discovery::dns::DnsDiscovery::n0_dns())
198 });
199 }
200 } else {
201 warn!(
202 target: LOG_NET_IROH,
203 "Iroh n0 discovery is disabled"
204 );
205 }
206 }
207
208 let endpoint = builder
209 .transport_config(quic_transport_config())
210 .bind()
211 .await
212 .map_err(|err| ConnectorError::Transport(err.into()))?;
213 debug!(
214 target: LOG_NET_IROH,
215 node_id = %endpoint.node_id(),
216 node_id_pkarr = %z32::encode(endpoint.node_id().as_bytes()),
217 "Iroh api client endpoint (stable)"
218 );
219 Ok::<_, ConnectorError>(endpoint)
220 }
221 });
222 let endpoint_next = Box::pin(async {
223 let mut builder = iroh_next::Endpoint::builder(iroh_next::endpoint::presets::Minimal);
224
225 if let Some(iroh_dns) = iroh_dns.map(SafeUrl::to_unsafe) {
226 builder = builder
227 .address_lookup(iroh_next::address_lookup::PkarrResolver::builder(iroh_dns));
228 }
229
230 let mut builder = builder.relay_mode(iroh_next::RelayMode::Default);
235
236 #[cfg(not(target_family = "wasm"))]
237 if iroh_enable_dht {
238 builder = builder
239 .address_lookup(iroh_mainline_address_lookup::DhtAddressLookup::builder());
240 }
241
242 {
245 builder =
247 builder.address_lookup(iroh_next::address_lookup::PkarrResolver::n0_dns());
248 #[cfg(not(target_family = "wasm"))]
250 {
251 builder = builder
252 .address_lookup(iroh_next::address_lookup::DnsAddressLookup::n0_dns());
253 }
254 }
255
256 let endpoint = builder
257 .transport_config(quic_transport_config_next())
258 .bind()
259 .await
260 .map_err(|err| ConnectorError::Transport(err.into()))?;
261 debug!(
262 target: LOG_NET_IROH,
263 node_id = %endpoint.id(),
264 node_id_pkarr = %z32::encode(endpoint.id().as_bytes()),
265 "Iroh api client endpoint (next)"
266 );
267 Ok(endpoint)
268 });
269
270 let (endpoint_stable, endpoint_next) = tokio::try_join!(endpoint_stable, endpoint_next)?;
271
272 Ok(Self {
273 stable: endpoint_stable,
274 next: endpoint_next,
275 connection_overrides: BTreeMap::new(),
276 path_change,
277 next_connections: Arc::new(Mutex::new(BTreeMap::new())),
278 })
279 }
280
281 pub(crate) fn with_connection_override(mut self, node: NodeId, addr: NodeAddr) -> Self {
282 self.connection_overrides.insert(node, addr);
283 self
284 }
285
286 pub(crate) fn node_id_from_url(url: &SafeUrl) -> Result<NodeId, ConnectorError> {
287 if url.scheme() != "iroh" {
288 return Err(ConnectorError::UnsupportedScheme {
289 scheme: url.scheme().to_owned(),
290 });
291 }
292 let host = url.host_str().ok_or_else(|| ConnectorError::MissingHost {
293 url: url.to_owned(),
294 })?;
295
296 PublicKey::from_str(host).map_err(|source| ConnectorError::InvalidNodeId {
297 host: host.to_owned(),
298 source: Box::new(source),
299 })
300 }
301}
302
303#[async_trait::async_trait]
304impl crate::Connector for IrohConnector {
305 async fn connect_guardian(
306 &self,
307 url: &SafeUrl,
308 api_secret: Option<&str>,
309 ) -> ServerResult<DynGuaridianConnection> {
310 if api_secret.is_some() {
311 return Err(ServerError::Connection(
315 "Iroh api secrets currently not supported".into(),
316 ));
317 }
318 let node_id =
319 Self::node_id_from_url(url).map_err(|source| ServerError::InvalidPeerUrl {
320 source: Box::new(source),
321 url: url.to_owned(),
322 })?;
323 let next_only = crate::is_iroh_next_endpoint_url(url).map_err(|source| {
324 ServerError::InvalidPeerUrl {
325 source: Box::new(source),
326 url: url.to_owned(),
327 }
328 })?;
329 let mut futures = FuturesUnordered::<
330 Pin<
331 Box<
332 dyn Future<Output = (ServerResult<DynGuaridianConnection>, &'static str)>
333 + Send,
334 >,
335 >,
336 >::new();
337 let connection_override = self.connection_overrides.get(&node_id).cloned();
338
339 if next_only {
342 return self
343 .make_new_connection_next(&self.next, node_id, connection_override)
344 .await
345 .map(super::IGuardianConnection::into_dyn);
346 }
347
348 let self_clone = self.clone();
349 futures.push(Box::pin({
350 let connection_override = connection_override.clone();
351 async move {
352 (
353 self_clone
354 .make_new_connection_stable(node_id, connection_override)
355 .await
356 .map(super::IGuardianConnection::into_dyn),
357 "stable",
358 )
359 }
360 }));
361
362 let self_clone = self.clone();
363 let endpoint_next = self.next.clone();
364 futures.push(Box::pin(async move {
365 (
366 self_clone
367 .make_new_connection_next(&endpoint_next, node_id, connection_override)
368 .await
369 .map(super::IGuardianConnection::into_dyn),
370 "next",
371 )
372 }));
373
374 let mut prev_err = None;
377
378 while let Some((result, iroh_stack)) = futures.next().await {
380 match result {
381 Ok(connection) => return Ok(connection),
382 Err(err) => {
383 warn!(
384 target: LOG_NET_IROH,
385 err = %err.fmt_compact(),
386 %iroh_stack,
387 "Join error in iroh connection task"
388 );
389 prev_err = Some(err);
390 }
391 }
392 }
393
394 Err(prev_err.unwrap_or_else(|| {
395 ServerError::ServerError("Both iroh connection attempts failed".to_string())
396 }))
397 }
398
399 async fn connect_gateway(&self, url: &SafeUrl) -> Result<DynGatewayConnection, ConnectorError> {
400 let node_id = Self::node_id_from_url(url)?;
401 if let Some(node_addr) = self.connection_overrides.get(&node_id).cloned() {
402 let conn = self
403 .stable
404 .connect(node_addr.clone(), FEDIMINT_GATEWAY_ALPN)
405 .await
406 .map_err(|err| ConnectorError::Transport(err.into()))?;
407
408 #[cfg(not(target_family = "wasm"))]
409 Self::spawn_connection_monitoring_stable(
410 &self.stable,
411 node_id,
412 self.path_change.clone(),
413 );
414
415 Ok(IGatewayConnection::into_dyn(conn))
416 } else {
417 let conn = self
418 .stable
419 .connect(node_id, FEDIMINT_GATEWAY_ALPN)
420 .await
421 .map_err(|err| ConnectorError::Transport(err.into()))?;
422 Ok(IGatewayConnection::into_dyn(conn))
423 }
424 }
425
426 fn connectivity(&self, url: &SafeUrl) -> Connectivity {
427 let Ok(node_id) = Self::node_id_from_url(url) else {
428 return Connectivity::Unknown;
429 };
430
431 if let Some(connectivity) = self.connectivity_next(node_id) {
437 return connectivity;
438 }
439
440 let Ok(watcher) = self.stable.conn_type(node_id) else {
441 return Connectivity::Unknown;
442 };
443 match watcher.get() {
444 Ok(iroh::endpoint::ConnectionType::Direct(_)) => Connectivity::Direct,
445 Ok(iroh::endpoint::ConnectionType::Relay(_)) => Connectivity::Relay,
446 Ok(iroh::endpoint::ConnectionType::Mixed(..)) => Connectivity::Mixed,
447 Ok(iroh::endpoint::ConnectionType::None) | Err(_) => Connectivity::Unknown,
448 }
449 }
450
451 async fn iroh_peer_info(
452 &self,
453 url: &SafeUrl,
454 path_timeout: Duration,
455 ) -> ServerResult<Option<IrohPeerInfo>> {
456 let node_id =
457 Self::node_id_from_url(url).map_err(|source| ServerError::InvalidPeerUrl {
458 source: Box::new(source),
459 url: url.to_owned(),
460 })?;
461 let connection_override = self.connection_overrides.get(&node_id).cloned();
462 let _connection = self
463 .make_new_connection_stable(node_id, connection_override)
464 .await?;
465
466 let mut conn_type_watcher = self
467 .stable
468 .conn_type(node_id)
469 .map_err(|err| ServerError::Connection(err.into()))?;
470 let mut conn_type = conn_type_watcher
471 .get()
472 .unwrap_or(iroh::endpoint::ConnectionType::None);
473
474 if path_timeout > Duration::ZERO {
475 let timeout = fedimint_core::runtime::sleep(path_timeout);
476 tokio::pin!(timeout);
477
478 while !matches!(
479 conn_type,
480 iroh::endpoint::ConnectionType::Direct(_)
481 | iroh::endpoint::ConnectionType::Mixed(..)
482 ) {
483 tokio::select! {
484 () = &mut timeout => break,
485 updated = conn_type_watcher.updated() => {
486 match updated {
487 Ok(updated) => conn_type = updated,
488 Err(_) => break,
489 }
490 }
491 }
492 }
493 }
494
495 Ok(Some(self.iroh_peer_info_from_conn_type(node_id, conn_type)))
496 }
497}
498
499impl IrohConnector {
500 fn connectivity_next(&self, node_id: NodeId) -> Option<Connectivity> {
508 let connections = self
509 .next_connections
510 .lock()
511 .expect("Next connection mutex is never held across a panic");
512
513 let connection = connections.get(&node_id)?;
514
515 if connection.close_reason().is_some() {
516 return None;
517 }
518
519 let paths = connection.paths();
523 let direct = paths.iter().any(|path| path.is_ip());
524 let relay = paths.iter().any(|path| path.is_relay());
525
526 Some(match (direct, relay) {
527 (true, true) => Connectivity::Mixed,
528 (true, false) => Connectivity::Direct,
529 (false, true) => Connectivity::Relay,
530 (false, false) => return None,
534 })
535 }
536
537 fn iroh_peer_info_from_conn_type(
538 &self,
539 node_id: NodeId,
540 conn_type: iroh::endpoint::ConnectionType,
541 ) -> IrohPeerInfo {
542 let remote_info = self.stable.remote_info(node_id);
543
544 let direct_addr = match &conn_type {
545 iroh::endpoint::ConnectionType::Direct(addr)
546 | iroh::endpoint::ConnectionType::Mixed(addr, _) => Some(*addr),
547 iroh::endpoint::ConnectionType::Relay(_) | iroh::endpoint::ConnectionType::None => None,
548 };
549
550 let mut known_direct_addrs = remote_info
551 .as_ref()
552 .map(|info| {
553 info.addrs
554 .iter()
555 .map(|addr_info| addr_info.addr)
556 .collect::<BTreeSet<_>>()
557 })
558 .unwrap_or_default();
559 if let Some(direct_addr) = direct_addr {
560 known_direct_addrs.insert(direct_addr);
561 }
562
563 let relay_url = match &conn_type {
564 iroh::endpoint::ConnectionType::Relay(relay_url)
565 | iroh::endpoint::ConnectionType::Mixed(_, relay_url) => Some(relay_url.to_string()),
566 iroh::endpoint::ConnectionType::Direct(_) | iroh::endpoint::ConnectionType::None => {
567 remote_info.and_then(|info| info.relay_url.map(|relay| relay.relay_url.to_string()))
568 }
569 };
570
571 IrohPeerInfo {
572 node_id: node_id.to_string(),
573 connectivity: connectivity_from_iroh_conn_type(&conn_type),
574 direct_addr,
575 known_direct_addrs: known_direct_addrs.into_iter().collect(),
576 relay_url,
577 }
578 }
579
580 #[cfg(not(target_family = "wasm"))]
581 fn spawn_connection_monitoring_stable(
582 endpoint: &Endpoint,
583 node_id: NodeId,
584 path_change: Arc<watch::Sender<u64>>,
585 ) {
586 if let Ok(mut conn_type_watcher) = endpoint.conn_type(node_id) {
587 #[allow(clippy::let_underscore_future)]
588 let _ = spawn("iroh connection (stable)", async move {
589 if let Ok(conn_type) = conn_type_watcher.get() {
590 debug!(target: LOG_NET_IROH, %node_id, type = %conn_type, "Connection type (initial)");
591 }
592 while let Ok(event) = conn_type_watcher.updated().await {
593 debug!(target: LOG_NET_IROH, %node_id, type = %event, "Connection type (changed)");
594 path_change.send_modify(|c| *c = c.wrapping_add(1));
595 }
596 });
597 }
598 }
599
600 #[cfg(not(target_family = "wasm"))]
601 fn spawn_connection_monitoring_next(
602 conn: &iroh_next::endpoint::Connection,
603 node_id: iroh_next::EndpointId,
604 path_change: Arc<watch::Sender<u64>>,
605 ) {
606 let conn = conn.clone();
607 #[allow(clippy::let_underscore_future)]
608 let _ = spawn("iroh connection (next)", async move {
609 let mut paths = conn.paths_stream();
610 if let Some(paths) = paths.next().await {
611 debug!(target: LOG_NET_IROH, %node_id, ?paths, "Connection paths (initial)");
612 }
613 while let Some(paths) = paths.next().await {
614 debug!(target: LOG_NET_IROH, %node_id, ?paths, "Connection paths changed");
615 path_change.send_modify(|c| *c = c.wrapping_add(1));
616 }
617 });
618 }
619
620 async fn make_new_connection_stable(
621 &self,
622 node_id: NodeId,
623 node_addr: Option<NodeAddr>,
624 ) -> ServerResult<Connection> {
625 trace!(target: LOG_NET_IROH, %node_id, "Creating new stable connection");
626 let conn = match node_addr.clone() {
627 Some(node_addr) => {
628 trace!(target: LOG_NET_IROH, %node_id, "Using a connectivity override for connection");
629 let conn = self.stable
630 .connect(node_addr.clone(), FEDIMINT_API_ALPN)
631 .await;
632
633 #[cfg(not(target_family = "wasm"))]
634 if conn.is_ok() {
635 Self::spawn_connection_monitoring_stable(
636 &self.stable,
637 node_id,
638 self.path_change.clone(),
639 );
640 }
641 conn
642 }
643 None => self.stable.connect(node_id, FEDIMINT_API_ALPN).await,
644 }.map_err(|err| ServerError::Connection(err.into()))?;
645
646 Ok(conn)
647 }
648
649 async fn make_new_connection_next(
650 &self,
651 endpoint_next: &iroh_next::Endpoint,
652 node_id: NodeId,
653 node_addr: Option<NodeAddr>,
654 ) -> ServerResult<iroh_next::endpoint::Connection> {
655 let next_node_id =
656 iroh_next::EndpointId::from_bytes(node_id.as_bytes()).expect("Can't fail");
657
658 let endpoint_next = endpoint_next.clone();
659
660 trace!(target: LOG_NET_IROH, %node_id, "Creating new next connection");
661 let conn = match node_addr.clone() {
662 Some(node_addr) => {
663 trace!(target: LOG_NET_IROH, %node_id, "Using a connectivity override for connection");
664 let node_addr = node_addr_stable_to_next(&node_addr);
665 let conn = endpoint_next
666 .connect(node_addr.clone(), FEDIMINT_API_ALPN)
667 .await;
668
669 #[cfg(not(target_family = "wasm"))]
670 if let Ok(conn) = &conn {
671 Self::spawn_connection_monitoring_next(
672 conn,
673 node_addr.id,
674 self.path_change.clone(),
675 );
676 }
677
678 conn
679 }
680 None => endpoint_next.connect(
681 next_node_id,
682 FEDIMINT_API_ALPN
683 ).await,
684 }
685 .map_err(|err| ServerError::Connection(err.into()))?;
686
687 self.next_connections
690 .lock()
691 .expect("Next connection mutex is never held across a panic")
692 .insert(node_id, conn.clone());
693
694 Ok(conn)
695 }
696}
697
698fn quic_transport_config() -> iroh::endpoint::TransportConfig {
701 let mut config = iroh::endpoint::TransportConfig::default();
702 config.max_idle_timeout(Some(
703 IROH_IDLE_TIMEOUT
704 .try_into()
705 .expect("idle timeout fits in IdleTimeout"),
706 ));
707 config.keep_alive_interval(Some(IROH_KEEP_ALIVE_INTERVAL));
708 config
709}
710
711fn quic_transport_config_next() -> iroh_next::endpoint::QuicTransportConfig {
714 iroh_next::endpoint::QuicTransportConfig::builder()
715 .max_idle_timeout(Some(
716 IROH_IDLE_TIMEOUT
717 .try_into()
718 .expect("idle timeout fits in IdleTimeout"),
719 ))
720 .keep_alive_interval(IROH_KEEP_ALIVE_INTERVAL)
721 .build()
722}
723
724fn connectivity_from_iroh_conn_type(conn_type: &iroh::endpoint::ConnectionType) -> Connectivity {
725 match conn_type {
726 iroh::endpoint::ConnectionType::Direct(_) => Connectivity::Direct,
727 iroh::endpoint::ConnectionType::Relay(_) => Connectivity::Relay,
728 iroh::endpoint::ConnectionType::Mixed(..) => Connectivity::Mixed,
729 iroh::endpoint::ConnectionType::None => Connectivity::Unknown,
730 }
731}
732
733fn node_addr_stable_to_next(stable: &iroh::NodeAddr) -> iroh_next::EndpointAddr {
734 let next_node_id =
735 iroh_next::EndpointId::from_bytes(stable.node_id.as_bytes()).expect("Can't fail");
736 let relay_addrs = stable.relay_url.iter().map(|u| {
737 iroh_next::TransportAddr::Relay(
738 iroh_next::RelayUrl::from_str(&u.to_string()).expect("Can't fail"),
739 )
740 });
741 let direct_addrs = stable
742 .direct_addresses
743 .iter()
744 .copied()
745 .map(iroh_next::TransportAddr::Ip);
746
747 iroh_next::EndpointAddr::from_parts(next_node_id, relay_addrs.chain(direct_addrs))
748}
749
750#[apply(async_trait_maybe_send!)]
751impl IConnection for Connection {
752 async fn await_disconnection(&self) {
753 self.closed().await;
754 }
755
756 fn is_connected(&self) -> bool {
757 self.close_reason().is_none()
758 }
759}
760
761#[async_trait]
762impl IGuardianConnection for Connection {
763 async fn request(&self, method: ApiMethod, request: ApiRequestErased) -> ServerResult<Value> {
764 let timeout = request_timeout_for_method(&method);
765 let method_str = method.to_string();
766 let json = serde_json::to_vec(&IrohApiRequest { method, request })
767 .expect("Serialization to vec can't fail");
768
769 let result = fedimint_core::runtime::timeout(timeout, async {
770 let (mut sink, mut stream) = self
771 .open_bi()
772 .await
773 .map_err(|e| ServerError::Transport(e.into()))?;
774
775 sink.write_all(&json)
776 .await
777 .map_err(|e| ServerError::Transport(e.into()))?;
778
779 sink.finish()
780 .map_err(|e| ServerError::Transport(e.into()))?;
781
782 stream
783 .read_to_end(IROH_MAX_RESPONSE_BYTES)
784 .await
785 .map_err(|e| ServerError::Transport(e.into()))
786 })
787 .await;
788
789 let response = match result {
790 Ok(Ok(bytes)) => bytes,
791 Ok(Err(err)) => return Err(err),
792 Err(_) => {
793 warn!(
800 target: LOG_NET_IROH,
801 method = %method_str,
802 timeout_secs = timeout.as_secs(),
803 "iroh request timed out, closing connection",
804 );
805 self.close(
806 iroh::endpoint::VarInt::from_u32(IROH_REQUEST_TIMEOUT_ERROR_CODE),
807 IROH_REQUEST_TIMEOUT_ERROR_REASON,
808 );
809 return Err(ServerError::Transport(
810 format!("iroh request {method_str} timed out after {timeout:?}").into(),
811 ));
812 }
813 };
814
815 let response = serde_json::from_slice::<Result<Value, ApiError>>(&response)
817 .map_err(|e| ServerError::InvalidResponse(e.fmt_compact().to_string()))?;
818
819 response.map_err(|e| ServerError::InvalidResponse(format!("Api Error: {e:?}")))
820 }
821}
822
823#[apply(async_trait_maybe_send!)]
824impl IConnection for iroh_next::endpoint::Connection {
825 async fn await_disconnection(&self) {
826 self.closed().await;
827 }
828
829 fn is_connected(&self) -> bool {
830 self.close_reason().is_none()
831 }
832}
833
834#[async_trait]
835impl IGuardianConnection for iroh_next::endpoint::Connection {
836 async fn request(&self, method: ApiMethod, request: ApiRequestErased) -> ServerResult<Value> {
837 let timeout = request_timeout_for_method(&method);
838 let method_str = method.to_string();
839 let json = serde_json::to_vec(&IrohApiRequest { method, request })
840 .expect("Serialization to vec can't fail");
841
842 let result = fedimint_core::runtime::timeout(timeout, async {
843 let (mut sink, mut stream) = self
844 .open_bi()
845 .await
846 .map_err(|e| ServerError::Transport(e.into()))?;
847
848 sink.write_all(&json)
849 .await
850 .map_err(|e| ServerError::Transport(e.into()))?;
851
852 sink.finish()
853 .map_err(|e| ServerError::Transport(e.into()))?;
854
855 stream
856 .read_to_end(IROH_MAX_RESPONSE_BYTES)
857 .await
858 .map_err(|e| ServerError::Transport(e.into()))
859 })
860 .await;
861
862 let response = match result {
863 Ok(Ok(bytes)) => bytes,
864 Ok(Err(err)) => return Err(err),
865 Err(_) => {
866 warn!(
867 target: LOG_NET_IROH,
868 method = %method_str,
869 timeout_secs = timeout.as_secs(),
870 "iroh request timed out, closing connection",
871 );
872 self.close(
873 iroh_next::endpoint::VarInt::from_u32(IROH_REQUEST_TIMEOUT_ERROR_CODE),
874 IROH_REQUEST_TIMEOUT_ERROR_REASON,
875 );
876 return Err(ServerError::Transport(
877 format!("iroh request {method_str} timed out after {timeout:?}").into(),
878 ));
879 }
880 };
881
882 let response = serde_json::from_slice::<Result<Value, ApiError>>(&response)
884 .map_err(|e| ServerError::InvalidResponse(e.fmt_compact().to_string()))?;
885
886 response.map_err(|e| ServerError::InvalidResponse(format!("Api Error: {e:?}")))
887 }
888}
889
890#[apply(async_trait_maybe_send!)]
891impl IGatewayConnection for Connection {
892 async fn request(
893 &self,
894 password: Option<String>,
895 _method: Method,
896 route: &str,
897 payload: Option<Value>,
898 ) -> ServerResult<Value> {
899 let iroh_request = IrohGatewayRequest {
900 route: route.to_string(),
901 params: payload,
902 password,
903 };
904 let json = serde_json::to_vec(&iroh_request).expect("serialization cant fail");
905
906 let (mut sink, mut stream) = self
907 .open_bi()
908 .await
909 .map_err(|e| ServerError::Transport(e.into()))?;
910
911 sink.write_all(&json)
912 .await
913 .map_err(|e| ServerError::Transport(e.into()))?;
914
915 sink.finish()
916 .map_err(|e| ServerError::Transport(e.into()))?;
917
918 let response = stream
919 .read_to_end(IROH_MAX_RESPONSE_BYTES)
920 .await
921 .map_err(|e| ServerError::Transport(e.into()))?;
922
923 let response = serde_json::from_slice::<IrohGatewayResponse>(&response)
924 .map_err(|e| ServerError::InvalidResponse(e.fmt_compact().to_string()))?;
925 match StatusCode::from_u16(response.status)
926 .map_err(|e| ServerError::InvalidResponse(format!("Invalid status code: {e}")))?
927 {
928 StatusCode::OK => Ok(response.body),
929 status => Err(ServerError::ServerError(format!(
930 "Server returned status code: {status}"
931 ))),
932 }
933 }
934}
935
936#[cfg(test)]
937mod tests;