mithril_aggregator_discovery/
http_config_discoverer.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
16struct NetworksConfigMessage {
17 #[serde(flatten)]
18 pub networks: HashMap<String, NetworkEnvironmentMessage>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23struct NetworkEnvironmentMessage {
24 #[serde(rename = "mithril-networks")]
25 pub mithril_networks: Vec<HashMap<String, MithrilNetworkMessage>>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30struct MithrilNetworkMessage {
31 pub aggregators: Vec<AggregatorMessage>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36struct AggregatorMessage {
37 pub url: String,
38}
39
40pub struct HttpConfigAggregatorDiscoverer {
44 configuration_file_url: String,
45}
46
47impl HttpConfigAggregatorDiscoverer {
48 const HTTP_TIMEOUT: Duration = Duration::from_secs(10);
49
50 pub fn new(configuration_file_url: &str) -> Self {
52 Self {
53 configuration_file_url: configuration_file_url.to_string(),
54 }
55 }
56
57 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}