Skip to content

Commit 3657df1

Browse files
authored
Merge pull request #2661 from input-output-hk/ensemble/2640/shared-aggretator-client
add shared aggregator client
2 parents cdc4548 + 70c6c51 commit 3657df1

File tree

24 files changed

+1293
-14
lines changed

24 files changed

+1293
-14
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -844,8 +844,8 @@ jobs:
844844
# the same name (we only want to document those anyway)
845845
cargo doc --no-deps --lib -p mithril-stm -p mithril-common \
846846
-p mithril-cardano-node-chain -p mithril-cardano-node-internal-database \
847-
-p mithril-dmq \
848-
-p mithril-build-script -p mithril-cli-helper -p mithril-doc -p mithril-doc-derive \
847+
-p mithril-aggregator-client -p mithril-build-script -p mithril-cli-helper \
848+
-p mithril-dmq -p mithril-doc -p mithril-doc-derive \
849849
-p mithril-era -p mithril-metric -p mithril-persistence -p mithril-resource-pool \
850850
-p mithril-ticker -p mithril-signed-entity-lock -p mithril-signed-entity-preloader \
851851
-p mithril-aggregator -p mithril-signer -p mithril-client -p mithril-client-cli \

Cargo.lock

Lines changed: 24 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ members = [
1111
"examples/client-mithril-stake-distribution",
1212
"internal/cardano-node/mithril-cardano-node-chain",
1313
"internal/cardano-node/mithril-cardano-node-internal-database",
14+
"internal/mithril-aggregator-client",
1415
"internal/mithril-build-script",
1516
"internal/mithril-cli-helper",
1617
"internal/mithril-dmq",

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,13 @@ This repository consists of the following parts:
8080
- [**Mithril signer**](./mithril-signer): the node of the **Mithril network** responsible for producing individual signatures that are collected and aggregated by the **Mithril aggregator**.
8181

8282
- [**Internal**](./internal): the shared tools and API used by **Mithril** crates.
83+
- [**Mithril aggregator client**](./internal/mithril-aggregator-client): a client to request data from a Mithril Aggregator, used by **Mithril network** nodes and client library.
84+
8385
- [**Mithril build script**](./internal/mithril-build-script): a toolbox for Mithril crates that uses a build script phase.
8486

8587
- [**Mithril cardano-node-chain**](./internal/cardano-node/mithril-cardano-node-chain): mechanisms to read and interact with the **Cardano chain** through a Cardano node, used by **Mithril network** nodes.
8688

87-
- [**Mithril cardano-node-internal-database**](./internal/cardano-node/mithril-cardano-node-internal-database): mechanisms to read the files of a **Cardano node** internal database and compute digests from them, used by **Mithril network** nodes.
89+
- [**Mithril cardano-node-internal-database**](./internal/cardano-node/mithril-cardano-node-internal-database): mechanisms to read the files of a **Cardano node** internal database and compute digests from them, used by **Mithril network** nodes and client library.
8890

8991
- [**Mithril cli helper**](./internal/mithril-cli-helper): **CLI** tools for **Mithril** binaries.
9092

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[package]
2+
name = "mithril-aggregator-client"
3+
version = "0.1.0"
4+
description = "Client to request data from a Mithril Aggregator"
5+
authors.workspace = true
6+
documentation.workspace = true
7+
edition.workspace = true
8+
homepage.workspace = true
9+
license.workspace = true
10+
repository.workspace = true
11+
include = ["**/*.rs", "Cargo.toml", "README.md"]
12+
13+
[lib]
14+
crate-type = ["lib", "cdylib", "staticlib"]
15+
16+
[dependencies]
17+
anyhow = { workspace = true }
18+
async-trait = { workspace = true }
19+
mithril-common = { path = "../../mithril-common", version = ">=0.5" }
20+
reqwest = { workspace = true }
21+
semver = { workspace = true }
22+
serde = { workspace = true }
23+
serde_json = { workspace = true }
24+
slog = { workspace = true }
25+
thiserror = { workspace = true }
26+
tokio = { workspace = true }
27+
28+
[dev-dependencies]
29+
http = "1.3.1"
30+
httpmock = "0.7.0"
31+
mithril-common = { path = "../../mithril-common", version = ">=0.5", features = ["test_tools"] }
32+
mockall = { workspace = true }
33+
slog-async = { workspace = true }
34+
slog-term = { workspace = true }
35+
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.PHONY: all build test check doc
2+
3+
CARGO = cargo
4+
5+
all: test build
6+
7+
build:
8+
${CARGO} build --release
9+
10+
test:
11+
${CARGO} test
12+
13+
check:
14+
${CARGO} check --release --all-features --all-targets
15+
${CARGO} clippy --release --all-features --all-targets
16+
${CARGO} fmt --check
17+
18+
doc:
19+
${CARGO} doc --no-deps --open
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Mithril-aggregator-client [![CI workflow](https://github.com/input-output-hk/mithril/actions/workflows/ci.yml/badge.svg)](https://github.com/input-output-hk/mithril/actions/workflows/ci.yml) [![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](https://github.com/input-output-hk/mithril/blob/main/LICENSE) [![Discord](https://img.shields.io/discord/500028886025895936.svg?logo=discord&style=flat-square)](https://discord.gg/5kaErDKDRq)
2+
3+
This crate provides a client to request data from a Mithril Aggregator.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
use anyhow::Context;
2+
use reqwest::{Client, IntoUrl, Proxy, Url};
3+
use slog::{Logger, o};
4+
use std::collections::HashMap;
5+
use std::time::Duration;
6+
7+
use mithril_common::StdResult;
8+
use mithril_common::api_version::APIVersionProvider;
9+
10+
use crate::client::AggregatorClient;
11+
12+
/// A builder of [AggregatorClient]
13+
pub struct AggregatorClientBuilder {
14+
aggregator_url_result: reqwest::Result<Url>,
15+
api_version_provider: Option<APIVersionProvider>,
16+
additional_headers: Option<HashMap<String, String>>,
17+
timeout_duration: Option<Duration>,
18+
relay_endpoint: Option<String>,
19+
logger: Option<Logger>,
20+
}
21+
22+
impl AggregatorClientBuilder {
23+
/// Constructs a new `AggregatorClientBuilder`.
24+
//
25+
// This is the same as `AggregatorClient::builder()`.
26+
pub fn new<U: IntoUrl>(aggregator_url: U) -> Self {
27+
Self {
28+
aggregator_url_result: aggregator_url.into_url(),
29+
api_version_provider: None,
30+
additional_headers: None,
31+
timeout_duration: None,
32+
relay_endpoint: None,
33+
logger: None,
34+
}
35+
}
36+
37+
/// Set the [Logger] to use.
38+
pub fn with_logger(mut self, logger: Logger) -> Self {
39+
self.logger = Some(logger);
40+
self
41+
}
42+
43+
/// Set the [APIVersionProvider] to use.
44+
pub fn with_api_version_provider(mut self, api_version_provider: APIVersionProvider) -> Self {
45+
self.api_version_provider = Some(api_version_provider);
46+
self
47+
}
48+
49+
/// Set a timeout to enforce on each request
50+
pub fn with_timeout(mut self, timeout: Duration) -> Self {
51+
self.timeout_duration = Some(timeout);
52+
self
53+
}
54+
55+
/// Add a set of http headers that will be sent on client requests
56+
pub fn with_headers(mut self, custom_headers: HashMap<String, String>) -> Self {
57+
self.additional_headers = Some(custom_headers);
58+
self
59+
}
60+
61+
/// Set the address of the relay
62+
pub fn with_relay_endpoint(mut self, relay_endpoint: String) -> Self {
63+
self.relay_endpoint = Some(relay_endpoint);
64+
self
65+
}
66+
67+
/// Returns an [AggregatorClient] based on the builder configuration
68+
pub fn build(self) -> StdResult<AggregatorClient> {
69+
let aggregator_endpoint =
70+
enforce_trailing_slash(self.aggregator_url_result.with_context(
71+
|| "Invalid aggregator endpoint, it must be a correctly formed url",
72+
)?);
73+
let logger = self.logger.unwrap_or_else(|| Logger::root(slog::Discard, o!()));
74+
let api_version_provider = self.api_version_provider.unwrap_or_default();
75+
let additional_headers = self.additional_headers.unwrap_or_default();
76+
let mut client_builder = Client::builder();
77+
78+
if let Some(relay_endpoint) = self.relay_endpoint {
79+
client_builder = client_builder
80+
.proxy(Proxy::all(relay_endpoint).with_context(|| "Relay proxy creation failed")?)
81+
}
82+
83+
Ok(AggregatorClient {
84+
aggregator_endpoint,
85+
api_version_provider,
86+
additional_headers: (&additional_headers)
87+
.try_into()
88+
.with_context(|| format!("Invalid headers: '{additional_headers:?}'"))?,
89+
timeout_duration: self.timeout_duration,
90+
client: client_builder
91+
.build()
92+
.with_context(|| "HTTP client creation failed")?,
93+
logger,
94+
})
95+
}
96+
}
97+
98+
fn enforce_trailing_slash(url: Url) -> Url {
99+
// Trailing slash is significant because url::join
100+
// (https://docs.rs/url/latest/url/struct.Url.html#method.join) will remove
101+
// the 'path' part of the url if it doesn't end with a trailing slash.
102+
if url.as_str().ends_with('/') {
103+
url
104+
} else {
105+
let mut url = url.clone();
106+
url.set_path(&format!("{}/", url.path()));
107+
url
108+
}
109+
}
110+
111+
#[cfg(test)]
112+
mod tests {
113+
use super::*;
114+
115+
#[test]
116+
fn enforce_trailing_slash_for_aggregator_url() {
117+
let url_without_trailing_slash = Url::parse("http://localhost:8080").unwrap();
118+
let url_with_trailing_slash = Url::parse("http://localhost:8080/").unwrap();
119+
120+
assert_eq!(
121+
url_with_trailing_slash,
122+
enforce_trailing_slash(url_without_trailing_slash.clone())
123+
);
124+
assert_eq!(
125+
url_with_trailing_slash,
126+
enforce_trailing_slash(url_with_trailing_slash.clone())
127+
);
128+
}
129+
}

0 commit comments

Comments
 (0)