mithril_aggregator_discovery/
http_config_discoverer.rs

1use std::{collections::HashMap, time::Duration};
2
3use anyhow::Context;
4use reqwest::Client;
5use serde::{Deserialize, Serialize};
6
7use mithril_common::{StdResult, entities::MithrilNetwork};
8
9use crate::{AggregatorDiscoverer, AggregatorEndpoint};
10
11const DEFAULT_REMOTE_NETWORKS_CONFIG_URL: &str =
12    "https://raw.githubusercontent.com/input-output-hk/mithril/main/networks.json";
13
14/// Representation of the networks configuration file.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16struct NetworksConfigMessage {
17    #[serde(flatten)]
18    pub networks: HashMap<String, NetworkEnvironmentMessage>,
19}
20
21/// Representation of a network environment in the networks configuration file.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23struct NetworkEnvironmentMessage {
24    #[serde(rename = "mithril-networks")]
25    pub mithril_networks: Vec<HashMap<String, MithrilNetworkMessage>>,
26}
27
28/// Representation of a Mithril network in the networks configuration file.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30struct MithrilNetworkMessage {
31    pub aggregators: Vec<AggregatorMessage>,
32}
33
34/// Representation of an aggregator in the networks configuration file.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36struct AggregatorMessage {
37    pub url: String,
38}
39
40/// An implementation of the [AggregatorDiscoverer] trait which discovers aggregators from remote networks configuration.
41///
42/// The reference file is the `networks.json` file hosted in the Mithril GitHub repository.
43pub struct HttpConfigAggregatorDiscoverer {
44    configuration_file_url: String,
45}
46
47impl HttpConfigAggregatorDiscoverer {
48    const HTTP_TIMEOUT: Duration = Duration::from_secs(10);
49
50    /// Creates a new `HttpConfigAggregatorDiscoverer` instance with the configuration file URL.
51    pub fn new(configuration_file_url: &str) -> Self {
52        Self {
53            configuration_file_url: configuration_file_url.to_string(),
54        }
55    }
56
57    /// Builds a reqwest HTTP client.
58    fn build_client(&self) -> StdResult<Client> {
59        Ok(Client::builder().timeout(Self::HTTP_TIMEOUT).build()?)
60    }
61}
62
63impl Default for HttpConfigAggregatorDiscoverer {
64    fn default() -> Self {
65        Self::new(DEFAULT_REMOTE_NETWORKS_CONFIG_URL)
66    }
67}
68
69#[async_trait::async_trait]
70impl AggregatorDiscoverer for HttpConfigAggregatorDiscoverer {
71    async fn get_available_aggregators(
72        &self,
73        network: MithrilNetwork,
74    ) -> StdResult<Box<dyn Iterator<Item = AggregatorEndpoint>>> {
75        let client = self.build_client()?;
76        let networks_configuration_response: NetworksConfigMessage = client
77            .get(&self.configuration_file_url)
78            .send()
79            .await
80            .with_context(|| {
81                format!(
82                    "AggregatorDiscovererHttpConfig failed retrieving configuration file from {}",
83                    &self.configuration_file_url
84                )
85            })?
86            .json::<NetworksConfigMessage>()
87            .await
88            .with_context(|| {
89                format!(
90                    "AggregatorDiscovererHttpConfig failed parsing configuration file from {}",
91                    &self.configuration_file_url
92                )
93            })?;
94        let aggregator_endpoints = networks_configuration_response
95            .networks
96            .values()
97            .flat_map(|env| &env.mithril_networks)
98            .flat_map(|network_map| network_map.iter())
99            .filter(|(name, _)| *name == network.name())
100            .flat_map(|(_, network)| &network.aggregators)
101            .map(|aggregator_msg| AggregatorEndpoint::new(aggregator_msg.url.clone()))
102            .collect::<Vec<_>>();
103
104        Ok(Box::new(aggregator_endpoints.into_iter()))
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use httpmock::MockServer;
111
112    use super::*;
113
114    const TEST_NETWORKS_CONFIG_JSON_SUCCESS: &str = r#"
115    {
116        "devnet": {
117            "mithril-networks": [
118                {
119                    "release-devnet": {
120                        "aggregators": [
121                            { "url": "https://release-devnet-aggregator1" },
122                            { "url": "https://release-devnet-aggregator2" }
123                        ]
124                    }
125                }
126            ]
127        },
128        "testnet": {
129            "mithril-networks": [
130                {
131                    "preview-testnet": {
132                        "aggregators": [
133                            { "url": "https://preview-testnet-aggregator1" },
134                            { "url": "https://preview-testnet-aggregator2" }
135                        ]
136                    }
137                }
138            ]
139        }
140    }"#;
141
142    const TEST_NETWORKS_CONFIG_JSON_FAILURE: &str = r#"
143    {
144        {"}
145    }"#;
146
147    fn create_server_and_discoverer(content: &str) -> (MockServer, HttpConfigAggregatorDiscoverer) {
148        let size = content.len() as u64;
149        let server = MockServer::start();
150        server.mock(|when, then| {
151            when.method(httpmock::Method::GET).path("/networks.json");
152            then.status(200)
153                .body(content)
154                .header(reqwest::header::CONTENT_LENGTH.as_str(), size.to_string());
155        });
156        let configuration_file_url = format!("{}{}", server.url("/"), "networks.json");
157        let discoverer = HttpConfigAggregatorDiscoverer::new(&configuration_file_url);
158
159        (server, discoverer)
160    }
161
162    #[tokio::test]
163    async fn get_available_aggregators_success() {
164        let content = TEST_NETWORKS_CONFIG_JSON_SUCCESS;
165        let (_server, discoverer) = create_server_and_discoverer(content);
166        let aggregators = discoverer
167            .get_available_aggregators(MithrilNetwork::new("release-devnet".into()))
168            .await
169            .unwrap();
170
171        assert_eq!(
172            vec![
173                AggregatorEndpoint::new("https://release-devnet-aggregator1".into()),
174                AggregatorEndpoint::new("https://release-devnet-aggregator2".into()),
175            ],
176            aggregators.collect::<Vec<_>>()
177        );
178
179        let mut aggregators = discoverer
180            .get_available_aggregators(MithrilNetwork::new("unknown".into()))
181            .await
182            .unwrap();
183
184        assert!(aggregators.next().is_none());
185    }
186
187    #[tokio::test]
188    async fn get_available_aggregators_failure() {
189        let content = TEST_NETWORKS_CONFIG_JSON_FAILURE;
190        let (_server, discoverer) = create_server_and_discoverer(content);
191        let result = discoverer
192            .get_available_aggregators(MithrilNetwork::new("release-devnet".into()))
193            .await;
194        assert!(
195            result.is_err(),
196            "The retrieval of the aggregators should fail"
197        );
198    }
199}