Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0554dbb
chore: scaffold `internal/mithril-aggregator-client` crate
Alenar Jul 29, 2025
a402fdb
feat: prototype aggregator client query system
Alenar Jul 30, 2025
bfc1b82
refactor(aggregator-client): move query related types to query module
Alenar Jul 30, 2025
6607cb0
test(aggregator-client): add aggregatorClientError tests
Alenar Jul 30, 2025
7ad948b
feat(aggregator-client): add `AggregatorClientBuilder`
Alenar Jul 30, 2025
c891308
test(aggregator-client): add a minimal post test
Alenar Jul 30, 2025
ae9e2d9
feat(common): impl `Default` to `APIVersionProvider`
Alenar Jul 30, 2025
d567e99
test(common): add `APIVersionProviderTestExtension`
Alenar Jul 30, 2025
f494875
feat(aggregator-client): add warning when client and aggregator api v…
Alenar Jul 30, 2025
f2f621a
feat(aggregator-client): send mithril api version header on all requests
Alenar Jul 30, 2025
50c9473
feat(aggregator-client): add timeout support
Alenar Jul 30, 2025
c8b90d2
doc(aggregator-client): add missing doc
Alenar Jul 30, 2025
8c5a9ee
feat(aggregator-client): add ability to set additional headers
Alenar Jul 31, 2025
cfa402c
feat(aggregator-client): add `ClientBuilder::with_relay_endpoint`
Alenar Jul 31, 2025
e9ba5ce
feat(aggregator-client): make the crate wasm compatible
Alenar Jul 31, 2025
20479ce
test(aggregator-client): simplify client tests
Alenar Jul 31, 2025
7131667
test(aggregator-client): add test to `CertificateDetailsQuery` and ge…
Alenar Jul 31, 2025
70c6c51
chore: upgrade crate versions
Alenar Jul 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -844,8 +844,8 @@ jobs:
# the same name (we only want to document those anyway)
cargo doc --no-deps --lib -p mithril-stm -p mithril-common \
-p mithril-cardano-node-chain -p mithril-cardano-node-internal-database \
-p mithril-dmq \
-p mithril-build-script -p mithril-cli-helper -p mithril-doc -p mithril-doc-derive \
-p mithril-aggregator-client -p mithril-build-script -p mithril-cli-helper \
-p mithril-dmq -p mithril-doc -p mithril-doc-derive \
-p mithril-era -p mithril-metric -p mithril-persistence -p mithril-resource-pool \
-p mithril-ticker -p mithril-signed-entity-lock -p mithril-signed-entity-preloader \
-p mithril-aggregator -p mithril-signer -p mithril-client -p mithril-client-cli \
Expand Down
27 changes: 24 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"examples/client-mithril-stake-distribution",
"internal/cardano-node/mithril-cardano-node-chain",
"internal/cardano-node/mithril-cardano-node-internal-database",
"internal/mithril-aggregator-client",
"internal/mithril-build-script",
"internal/mithril-cli-helper",
"internal/mithril-dmq",
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,13 @@ This repository consists of the following parts:
- [**Mithril signer**](./mithril-signer): the node of the **Mithril network** responsible for producing individual signatures that are collected and aggregated by the **Mithril aggregator**.

- [**Internal**](./internal): the shared tools and API used by **Mithril** crates.
- [**Mithril aggregator client**](./internal/mithril-aggregator-client): a client to request data from a Mithril Aggregator, used by **Mithril network** nodes and client library.

- [**Mithril build script**](./internal/mithril-build-script): a toolbox for Mithril crates that uses a build script phase.

- [**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.

- [**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.
- [**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.

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

Expand Down
35 changes: 35 additions & 0 deletions internal/mithril-aggregator-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[package]
name = "mithril-aggregator-client"
version = "0.1.0"
description = "Client to request data from a Mithril Aggregator"
authors.workspace = true
documentation.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
include = ["**/*.rs", "Cargo.toml", "README.md"]

[lib]
crate-type = ["lib", "cdylib", "staticlib"]

[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
mithril-common = { path = "../../mithril-common", version = ">=0.5" }
reqwest = { workspace = true }
semver = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
slog = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }

[dev-dependencies]
http = "1.3.1"
httpmock = "0.7.0"
mithril-common = { path = "../../mithril-common", version = ">=0.5", features = ["test_tools"] }
mockall = { workspace = true }
slog-async = { workspace = true }
slog-term = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
19 changes: 19 additions & 0 deletions internal/mithril-aggregator-client/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.PHONY: all build test check doc

CARGO = cargo

all: test build

build:
${CARGO} build --release

test:
${CARGO} test

check:
${CARGO} check --release --all-features --all-targets
${CARGO} clippy --release --all-features --all-targets
${CARGO} fmt --check

doc:
${CARGO} doc --no-deps --open
3 changes: 3 additions & 0 deletions internal/mithril-aggregator-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 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)

This crate provides a client to request data from a Mithril Aggregator.
129 changes: 129 additions & 0 deletions internal/mithril-aggregator-client/src/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
use anyhow::Context;
use reqwest::{Client, IntoUrl, Proxy, Url};
use slog::{Logger, o};
use std::collections::HashMap;
use std::time::Duration;

use mithril_common::StdResult;
use mithril_common::api_version::APIVersionProvider;

use crate::client::AggregatorClient;

/// A builder of [AggregatorClient]
pub struct AggregatorClientBuilder {
aggregator_url_result: reqwest::Result<Url>,
api_version_provider: Option<APIVersionProvider>,
additional_headers: Option<HashMap<String, String>>,
timeout_duration: Option<Duration>,
relay_endpoint: Option<String>,
logger: Option<Logger>,
}

impl AggregatorClientBuilder {
/// Constructs a new `AggregatorClientBuilder`.
//
// This is the same as `AggregatorClient::builder()`.
pub fn new<U: IntoUrl>(aggregator_url: U) -> Self {
Self {
aggregator_url_result: aggregator_url.into_url(),
api_version_provider: None,
additional_headers: None,
timeout_duration: None,
relay_endpoint: None,
logger: None,
}
}

/// Set the [Logger] to use.
pub fn with_logger(mut self, logger: Logger) -> Self {
self.logger = Some(logger);
self
}

/// Set the [APIVersionProvider] to use.
pub fn with_api_version_provider(mut self, api_version_provider: APIVersionProvider) -> Self {
self.api_version_provider = Some(api_version_provider);
self
}

/// Set a timeout to enforce on each request
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout_duration = Some(timeout);
self
}

/// Add a set of http headers that will be sent on client requests
pub fn with_headers(mut self, custom_headers: HashMap<String, String>) -> Self {
self.additional_headers = Some(custom_headers);
self
}

/// Set the address of the relay
pub fn with_relay_endpoint(mut self, relay_endpoint: String) -> Self {
self.relay_endpoint = Some(relay_endpoint);
self
}

/// Returns an [AggregatorClient] based on the builder configuration
pub fn build(self) -> StdResult<AggregatorClient> {
let aggregator_endpoint =
enforce_trailing_slash(self.aggregator_url_result.with_context(
|| "Invalid aggregator endpoint, it must be a correctly formed url",
)?);
let logger = self.logger.unwrap_or_else(|| Logger::root(slog::Discard, o!()));
let api_version_provider = self.api_version_provider.unwrap_or_default();
let additional_headers = self.additional_headers.unwrap_or_default();
let mut client_builder = Client::builder();

if let Some(relay_endpoint) = self.relay_endpoint {
client_builder = client_builder
.proxy(Proxy::all(relay_endpoint).with_context(|| "Relay proxy creation failed")?)
}

Ok(AggregatorClient {
aggregator_endpoint,
api_version_provider,
additional_headers: (&additional_headers)
.try_into()
.with_context(|| format!("Invalid headers: '{additional_headers:?}'"))?,
timeout_duration: self.timeout_duration,
client: client_builder
.build()
.with_context(|| "HTTP client creation failed")?,
logger,
})
}
}

fn enforce_trailing_slash(url: Url) -> Url {
// Trailing slash is significant because url::join
// (https://docs.rs/url/latest/url/struct.Url.html#method.join) will remove
// the 'path' part of the url if it doesn't end with a trailing slash.
if url.as_str().ends_with('/') {
url
} else {
let mut url = url.clone();
url.set_path(&format!("{}/", url.path()));
url
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn enforce_trailing_slash_for_aggregator_url() {
let url_without_trailing_slash = Url::parse("http://localhost:8080").unwrap();
let url_with_trailing_slash = Url::parse("http://localhost:8080/").unwrap();

assert_eq!(
url_with_trailing_slash,
enforce_trailing_slash(url_without_trailing_slash.clone())
);
assert_eq!(
url_with_trailing_slash,
enforce_trailing_slash(url_with_trailing_slash.clone())
);
}
}
Loading
Loading