fedimint_ln_common/
client.rs

1use std::collections::BTreeSet;
2use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
3use std::str::FromStr;
4
5use anyhow::Context;
6use fedimint_core::util::SafeUrl;
7use iroh::NodeAddr;
8use reqwest::{Method, StatusCode};
9use serde::Serialize;
10use serde::de::DeserializeOwned;
11use thiserror::Error;
12
13use crate::iroh::GatewayIrohConnector;
14
15pub struct GatewayRpcClient {
16    base_url: SafeUrl,
17    iroh_connector: Option<GatewayIrohConnector>,
18    client: reqwest::Client,
19    password: Option<String>,
20}
21
22impl GatewayRpcClient {
23    pub async fn new(
24        api: SafeUrl,
25        password: Option<String>,
26        iroh_dns: Option<SafeUrl>,
27        connection_override: Option<SafeUrl>,
28    ) -> anyhow::Result<Self> {
29        let iroh_connector = if api.scheme() == "iroh" {
30            let host = api.host_str().context("Url is missing host")?;
31            let iroh_pk = iroh::PublicKey::from_str(host).context(format!(
32                "Could not parse Iroh Public key: Invalid public key: {host}"
33            ))?;
34            let mut iroh_connector =
35                GatewayIrohConnector::new(iroh_pk, password.clone(), iroh_dns).await?;
36
37            if let Some(connection_override) = connection_override {
38                let node_addr = NodeAddr {
39                    node_id: iroh_pk,
40                    relay_url: None,
41                    direct_addresses: BTreeSet::from([SocketAddr::V4(SocketAddrV4::new(
42                        connection_override
43                            .host_str()
44                            .ok_or(anyhow::anyhow!("No connection override host"))?
45                            .parse::<Ipv4Addr>()?,
46                        connection_override.port().ok_or(anyhow::anyhow!(
47                            "No iroh port supplied for connection override"
48                        ))?,
49                    ))]),
50                };
51
52                iroh_connector = iroh_connector.with_connection_override(iroh_pk, node_addr);
53            }
54            Some(iroh_connector)
55        } else {
56            None
57        };
58
59        Ok(Self {
60            base_url: api,
61            iroh_connector,
62            client: reqwest::Client::new(),
63            password,
64        })
65    }
66
67    async fn call<P: Serialize, T: DeserializeOwned>(
68        &self,
69        method: Method,
70        route: &str,
71        payload: Option<P>,
72    ) -> Result<T, GatewayRpcError> {
73        if let Some(iroh_connector) = &self.iroh_connector {
74            let payload = payload.map(|p| serde_json::to_value(p).expect("Could not serialize"));
75            let response = iroh_connector
76                .request(route, payload)
77                .await
78                .map_err(|e| GatewayRpcError::IrohError(e.to_string()))?;
79            let status_code = StatusCode::from_u16(response.status)
80                .map_err(|e| GatewayRpcError::IrohError(e.to_string()))?;
81            match status_code {
82                StatusCode::OK => {
83                    let response = serde_json::from_value::<T>(response.body)
84                        .map_err(|e| GatewayRpcError::IrohError(e.to_string()))?;
85                    Ok(response)
86                }
87                status => Err(GatewayRpcError::BadStatus(status)),
88            }
89        } else {
90            let url = self.base_url.join(route).expect("Invalid base url");
91            let mut builder = self.client.request(method, url.clone().to_unsafe());
92            if let Some(password) = self.password.clone() {
93                builder = builder.bearer_auth(password);
94            }
95            if let Some(payload) = payload {
96                builder = builder.json(&payload);
97            }
98
99            let response = builder
100                .send()
101                .await
102                .map_err(|e| GatewayRpcError::RequestError(e.to_string()))?;
103
104            match response.status() {
105                StatusCode::OK => Ok(response
106                    .json::<T>()
107                    .await
108                    .map_err(|e| GatewayRpcError::RequestError(e.to_string()))?),
109                status => Err(GatewayRpcError::BadStatus(status)),
110            }
111        }
112    }
113
114    pub async fn call_get<T: DeserializeOwned>(&self, route: &str) -> Result<T, GatewayRpcError> {
115        self.call(Method::GET, route, None::<()>).await
116    }
117
118    pub async fn call_post<P: Serialize, T: DeserializeOwned>(
119        &self,
120        route: &str,
121        payload: P,
122    ) -> Result<T, GatewayRpcError> {
123        self.call(Method::POST, route, Some(payload)).await
124    }
125}
126
127pub type GatewayRpcResult<T> = Result<T, GatewayRpcError>;
128
129#[derive(Error, Debug, Clone, Eq, PartialEq)]
130pub enum GatewayRpcError {
131    #[error("Bad status returned {0}")]
132    BadStatus(StatusCode),
133    #[error("Error connecting to the gateway {0}")]
134    RequestError(String),
135    #[error("Iroh error: {0}")]
136    IrohError(String),
137}