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