Skip to content

Feat: implement client for CardanoDatabase in client library (list and get) #2255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ As a minor extension, we have adopted a slightly different versioning convention
- Implement the artifact ancillary builder in the aggregator.
- Implement the artifact immutable builder in the aggregator.
- Implement the artifact digest builder in the aggregator.
- Implement the client library for the the signed entity type `CardanoDatabase` (list snapshots and get snapshot detail).

- Crates versions:

Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion mithril-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mithril-client"
version = "0.10.8"
version = "0.10.9"
description = "Mithril client library"
authors = { workspace = true }
edition = { workspace = true }
Expand Down
39 changes: 39 additions & 0 deletions mithril-client/src/aggregator_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,25 @@ pub enum AggregatorRequest {
/// Hash of the certificate to retrieve
hash: String,
},

/// Lists the aggregator [certificates][crate::MithrilCertificate]
ListCertificates,

/// Get a specific [Mithril stake distribution][crate::MithrilStakeDistribution] from the aggregator
GetMithrilStakeDistribution {
/// Hash of the Mithril stake distribution to retrieve
hash: String,
},

/// Lists the aggregator [Mithril stake distribution][crate::MithrilStakeDistribution]
ListMithrilStakeDistributions,

/// Get a specific [snapshot][crate::Snapshot] from the aggregator
GetSnapshot {
/// Digest of the snapshot to retrieve
digest: String,
},

/// Lists the aggregator [snapshots][crate::Snapshot]
ListSnapshots,

Expand All @@ -77,6 +82,17 @@ pub enum AggregatorRequest {
snapshot: String,
},

/// Get a specific [Cardano database snapshot][crate::CardanoDatabaseSnapshot] from the aggregator
#[cfg(feature = "unstable")]
GetCardanoDatabaseSnapshot {
/// Hash of the snapshot to retrieve
hash: String,
},

/// Lists the aggregator [Cardano database snapshots][crate::CardanoDatabaseSnapshot]
#[cfg(feature = "unstable")]
ListCardanoDatabaseSnapshots,

/// Get proofs that the given set of Cardano transactions is included in the global Cardano transactions set
GetTransactionsProofs {
/// Hashes of the transactions to get proofs for.
Expand Down Expand Up @@ -129,6 +145,14 @@ impl AggregatorRequest {
AggregatorRequest::IncrementSnapshotStatistic { snapshot: _ } => {
"statistics/snapshot".to_string()
}
#[cfg(feature = "unstable")]
AggregatorRequest::GetCardanoDatabaseSnapshot { hash } => {
format!("artifact/cardano-database/{}", hash)
}
#[cfg(feature = "unstable")]
AggregatorRequest::ListCardanoDatabaseSnapshots => {
"artifact/cardano-database".to_string()
}
AggregatorRequest::GetTransactionsProofs {
transactions_hashes,
} => format!(
Expand Down Expand Up @@ -571,6 +595,21 @@ mod tests {
.route()
);

#[cfg(feature = "unstable")]
assert_eq!(
"artifact/cardano-database/abc".to_string(),
AggregatorRequest::GetCardanoDatabaseSnapshot {
hash: "abc".to_string()
}
.route()
);

#[cfg(feature = "unstable")]
assert_eq!(
"artifact/cardano-database".to_string(),
AggregatorRequest::ListCardanoDatabaseSnapshots.route()
);

assert_eq!(
"proof/cardano-transaction?transaction_hashes=abc,def,ghi,jkl".to_string(),
AggregatorRequest::GetTransactionsProofs {
Expand Down
249 changes: 249 additions & 0 deletions mithril-client/src/cardano_database_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
//! A client to retrieve Cardano databases data from an Aggregator.
//!
//! In order to do so it defines a [CardanoDatabaseClient] which exposes the following features:
//! - [get][CardanoDatabaseClient::get]: get a Cardano database data from its hash
//! - [list][CardanoDatabaseClient::list]: get the list of available Cardano database
//!
//! # Get a Cardano database
//!
//! To get a Cardano database using the [ClientBuilder][crate::client::ClientBuilder].
//!
//! ```no_run
//! # async fn run() -> mithril_client::MithrilResult<()> {
//! use mithril_client::ClientBuilder;
//!
//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
//! let cardano_database = client.cardano_database().get("CARDANO_DATABASE_HASH").await?.unwrap();
//!
//! println!(
//! "Cardano database hash={}, merkle_root={}, immutable_file_number={:?}",
//! cardano_database.hash,
//! cardano_database.merkle_root,
//! cardano_database.beacon.immutable_file_number
//! );
//! # Ok(())
//! # }
//! ```
//!
//! # List available Cardano databases
//!
//! To list available Cardano databases using the [ClientBuilder][crate::client::ClientBuilder].
//!
//! ```no_run
//! # async fn run() -> mithril_client::MithrilResult<()> {
//! use mithril_client::ClientBuilder;
//!
//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
//! let cardano_databases = client.cardano_database().list().await?;
//!
//! for cardano_database in cardano_databases {
//! println!("Cardano database hash={}, immutable_file_number={}", cardano_database.hash, cardano_database.beacon.immutable_file_number);
//! }
//! # Ok(())
//! # }
//! ```

use anyhow::Context;
use std::sync::Arc;

use crate::aggregator_client::{AggregatorClient, AggregatorClientError, AggregatorRequest};
use crate::{CardanoDatabaseSnapshot, CardanoDatabaseSnapshotListItem, MithrilResult};

/// HTTP client for CardanoDatabase API from the Aggregator
pub struct CardanoDatabaseClient {
aggregator_client: Arc<dyn AggregatorClient>,
}

impl CardanoDatabaseClient {
/// Constructs a new `CardanoDatabase`.
pub fn new(aggregator_client: Arc<dyn AggregatorClient>) -> Self {
Self { aggregator_client }
}

/// Fetch a list of signed CardanoDatabase
pub async fn list(&self) -> MithrilResult<Vec<CardanoDatabaseSnapshotListItem>> {
let response = self
.aggregator_client
.get_content(AggregatorRequest::ListCardanoDatabaseSnapshots)
.await
.with_context(|| "CardanoDatabase client can not get the artifact list")?;
let items = serde_json::from_str::<Vec<CardanoDatabaseSnapshotListItem>>(&response)
.with_context(|| "CardanoDatabase client can not deserialize artifact list")?;

Ok(items)
}

/// Get the given Cardano database data by hash.
pub async fn get(&self, hash: &str) -> MithrilResult<Option<CardanoDatabaseSnapshot>> {
self.fetch_with_aggregator_request(AggregatorRequest::GetCardanoDatabaseSnapshot {
hash: hash.to_string(),
})
.await
}

/// Fetch the given Cardano database data with an aggregator request.
/// If it cannot be found, a None is returned.
async fn fetch_with_aggregator_request(
&self,
request: AggregatorRequest,
) -> MithrilResult<Option<CardanoDatabaseSnapshot>> {
match self.aggregator_client.get_content(request).await {
Ok(content) => {
let cardano_database: CardanoDatabaseSnapshot = serde_json::from_str(&content)
.with_context(|| "CardanoDatabase client can not deserialize artifact")?;

Ok(Some(cardano_database))
}
Err(AggregatorClientError::RemoteServerLogical(_)) => Ok(None),
Err(e) => Err(e.into()),
}
}
}

#[cfg(test)]
mod tests {
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use mithril_common::entities::{CardanoDbBeacon, CompressionAlgorithm, Epoch};
use mockall::predicate::eq;

use crate::aggregator_client::MockAggregatorHTTPClient;

use super::*;

fn fake_messages() -> Vec<CardanoDatabaseSnapshotListItem> {
vec![
CardanoDatabaseSnapshotListItem {
hash: "hash-123".to_string(),
merkle_root: "mkroot-123".to_string(),
beacon: CardanoDbBeacon {
epoch: Epoch(1),
immutable_file_number: 123,
},
certificate_hash: "cert-hash-123".to_string(),
total_db_size_uncompressed: 800796318,
created_at: DateTime::parse_from_rfc3339("2025-01-19T13:43:05.618857482Z")
.unwrap()
.with_timezone(&Utc),
compression_algorithm: CompressionAlgorithm::default(),
cardano_node_version: "0.0.1".to_string(),
},
CardanoDatabaseSnapshotListItem {
hash: "hash-456".to_string(),
merkle_root: "mkroot-456".to_string(),
beacon: CardanoDbBeacon {
epoch: Epoch(2),
immutable_file_number: 456,
},
certificate_hash: "cert-hash-456".to_string(),
total_db_size_uncompressed: 2960713808,
created_at: DateTime::parse_from_rfc3339("2025-01-27T15:22:05.618857482Z")
.unwrap()
.with_timezone(&Utc),
compression_algorithm: CompressionAlgorithm::default(),
cardano_node_version: "0.0.1".to_string(),
},
]
}

#[tokio::test]
async fn list_cardano_database_snapshots_returns_messages() {
let message = fake_messages();
let mut http_client = MockAggregatorHTTPClient::new();
http_client
.expect_get_content()
.with(eq(AggregatorRequest::ListCardanoDatabaseSnapshots))
.return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
let client = CardanoDatabaseClient::new(Arc::new(http_client));

let messages = client.list().await.unwrap();

assert_eq!(2, messages.len());
assert_eq!("hash-123".to_string(), messages[0].hash);
assert_eq!("hash-456".to_string(), messages[1].hash);
}

#[tokio::test]
async fn list_cardano_database_snapshots_returns_error_when_invalid_json_structure_in_response()
{
let mut http_client = MockAggregatorHTTPClient::new();
http_client
.expect_get_content()
.return_once(move |_| Ok("invalid json structure".to_string()));
let client = CardanoDatabaseClient::new(Arc::new(http_client));

client
.list()
.await
.expect_err("List Cardano databases should return an error");
}

#[tokio::test]
async fn get_cardano_database_snapshot_returns_message() {
let expected_cardano_database_snapshot = CardanoDatabaseSnapshot {
hash: "hash-123".to_string(),
..CardanoDatabaseSnapshot::dummy()
};
let message = expected_cardano_database_snapshot.clone();
let mut http_client = MockAggregatorHTTPClient::new();
http_client
.expect_get_content()
.with(eq(AggregatorRequest::GetCardanoDatabaseSnapshot {
hash: "hash-123".to_string(),
}))
.return_once(move |_| Ok(serde_json::to_string(&message).unwrap()));
let client = CardanoDatabaseClient::new(Arc::new(http_client));

let cardano_database = client
.get("hash-123")
.await
.unwrap()
.expect("This test returns a Cardano database");

assert_eq!(expected_cardano_database_snapshot, cardano_database);
}

#[tokio::test]
async fn get_cardano_database_snapshot_returns_error_when_invalid_json_structure_in_response() {
let mut http_client = MockAggregatorHTTPClient::new();
http_client
.expect_get_content()
.return_once(move |_| Ok("invalid json structure".to_string()));
let client = CardanoDatabaseClient::new(Arc::new(http_client));

client
.get("hash-123")
.await
.expect_err("Get Cardano database should return an error");
}

#[tokio::test]
async fn get_cardano_database_snapshot_returns_none_when_not_found_or_remote_server_logical_error(
) {
let mut http_client = MockAggregatorHTTPClient::new();
http_client.expect_get_content().return_once(move |_| {
Err(AggregatorClientError::RemoteServerLogical(anyhow!(
"not found"
)))
});
let client = CardanoDatabaseClient::new(Arc::new(http_client));

let result = client.get("hash-123").await.unwrap();

assert!(result.is_none());
}

#[tokio::test]
async fn get_cardano_database_snapshot_returns_error() {
let mut http_client = MockAggregatorHTTPClient::new();
http_client
.expect_get_content()
.return_once(move |_| Err(AggregatorClientError::SubsystemError(anyhow!("error"))));
let client = CardanoDatabaseClient::new(Arc::new(http_client));

client
.get("hash-123")
.await
.expect_err("Get Cardano database should return an error");
}
}
4 changes: 2 additions & 2 deletions mithril-client/src/cardano_stake_distribution_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ mod tests {
}

#[tokio::test]
async fn list_mithril_stake_distributions_returns_messages() {
async fn list_cardano_stake_distributions_returns_messages() {
let message = fake_messages();
let mut http_client = MockAggregatorHTTPClient::new();
http_client
Expand All @@ -188,7 +188,7 @@ mod tests {
}

#[tokio::test]
async fn list_mithril_stake_distributions_returns_error_when_invalid_json_structure_in_response(
async fn list_cardano_stake_distributions_returns_error_when_invalid_json_structure_in_response(
) {
let mut http_client = MockAggregatorHTTPClient::new();
http_client
Expand Down
Loading
Loading