fedimint_ln_common/
iroh.rs

1use std::collections::BTreeMap;
2
3use fedimint_core::envs::{FM_IROH_ENABLE_DHT_ENV, is_env_var_set};
4use fedimint_core::iroh_prod::FM_IROH_DNS_FEDIMINT_PROD;
5use fedimint_core::util::SafeUrl;
6use fedimint_logging::LOG_NET_IROH;
7use iroh::discovery::pkarr::PkarrResolver;
8use iroh::endpoint::Connection;
9use iroh::{Endpoint, NodeAddr, NodeId};
10use serde::{Deserialize, Serialize};
11use tracing::{info, trace};
12
13pub const FEDIMINT_GATEWAY_ALPN: &[u8] = b"FEDIMINT_GATEWAY_ALPN";
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct IrohGatewayRequest {
17    /// REST API route for specifying which action to take
18    pub route: String,
19
20    /// Parameters for the request
21    pub params: Option<serde_json::Value>,
22
23    /// Password for authenticated requests to the gateway
24    pub password: Option<String>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct IrohGatewayResponse {
29    pub status: u16,
30    pub body: serde_json::Value,
31}
32
33#[derive(Debug, Clone)]
34pub struct GatewayIrohConnector {
35    node_id: iroh::NodeId,
36    endpoint: Endpoint,
37    password: Option<String>,
38    connection_overrides: BTreeMap<NodeId, NodeAddr>,
39}
40
41impl GatewayIrohConnector {
42    pub async fn new(
43        iroh_pk: iroh::PublicKey,
44        password: Option<String>,
45        iroh_dns: Option<SafeUrl>,
46    ) -> anyhow::Result<Self> {
47        let mut builder = Endpoint::builder();
48
49        let iroh_dns_servers: Vec<_> = iroh_dns.map_or_else(
50            || {
51                FM_IROH_DNS_FEDIMINT_PROD
52                    .into_iter()
53                    .map(|url| {
54                        SafeUrl::parse(url)
55                            .expect("Hardcoded, can't fail")
56                            .to_unsafe()
57                    })
58                    .collect()
59            },
60            |url| vec![url.to_unsafe()],
61        );
62
63        for iroh_dns in iroh_dns_servers {
64            builder = builder.add_discovery(|_| Some(PkarrResolver::new(iroh_dns)));
65        }
66
67        // As a client, we don't need to register on any relays
68        let mut builder = builder.relay_mode(iroh::RelayMode::Disabled);
69
70        // See <https://github.com/fedimint/fedimint/issues/7811>
71        if is_env_var_set(FM_IROH_ENABLE_DHT_ENV) {
72            #[cfg(not(target_family = "wasm"))]
73            {
74                builder = builder.discovery_dht();
75            }
76        } else {
77            info!(
78                target: LOG_NET_IROH,
79                "Iroh DHT is disabled"
80            );
81        }
82
83        // instead of `.discovery_n0`, which brings publisher we don't want
84        {
85            #[cfg(target_family = "wasm")]
86            {
87                builder = builder.add_discovery(move |_| Some(PkarrResolver::n0_dns()));
88            }
89
90            #[cfg(not(target_family = "wasm"))]
91            {
92                builder = builder
93                    .add_discovery(move |_| Some(iroh::discovery::dns::DnsDiscovery::n0_dns()));
94            }
95        }
96
97        let endpoint = builder.bind().await?;
98
99        Ok(Self {
100            node_id: iroh_pk,
101            endpoint,
102            password,
103            connection_overrides: BTreeMap::new(),
104        })
105    }
106
107    #[must_use]
108    pub fn with_connection_override(mut self, node: NodeId, addr: NodeAddr) -> Self {
109        self.connection_overrides.insert(node, addr);
110        self
111    }
112
113    async fn connect(&self) -> anyhow::Result<Connection> {
114        let connection = match self.connection_overrides.get(&self.node_id) {
115            Some(node_addr) => {
116                trace!(target: LOG_NET_IROH, node_id = %self.node_id, "Using a connectivity override for connection");
117                self.endpoint
118                    .connect(node_addr.clone(), FEDIMINT_GATEWAY_ALPN)
119                    .await?
120            }
121            None => {
122                self.endpoint
123                    .connect(self.node_id, FEDIMINT_GATEWAY_ALPN)
124                    .await?
125            }
126        };
127
128        // TODO: Spawn connection monitoring?
129        Ok(connection)
130    }
131
132    pub async fn request(
133        &self,
134        route: &str,
135        payload: Option<serde_json::Value>,
136    ) -> anyhow::Result<IrohGatewayResponse> {
137        let iroh_request = IrohGatewayRequest {
138            route: route.to_string(),
139            params: payload,
140            password: self.password.clone(),
141        };
142        let json = serde_json::to_vec(&iroh_request).expect("serialization cant fail");
143        let connection = self.connect().await?;
144        let (mut sink, mut stream) = connection.open_bi().await?;
145        sink.write_all(&json).await?;
146        sink.finish()?;
147        let response = stream.read_to_end(1_000_000).await?;
148        let iroh_response = serde_json::from_slice::<IrohGatewayResponse>(&response)?;
149        Ok(iroh_response)
150    }
151}