Skip to content
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

Implement Cardano stake distribution in mithril-client WASM #1887

Merged
Show file tree
Hide file tree
Changes from 7 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 @@ -17,6 +17,7 @@ As a minor extension, we have adopted a slightly different versioning convention
- Implement the HTTP routes related to the signed entity type `CardanoStakeDistribution` on the aggregator REST API.
- Added support in the `mithril-client` library for retrieving `CardanoStakeDistribution` by epoch or by hash, and for listing all available `CardanoStakeDistribution`.
- Implement `CardanoStakeDistribution` commands under the `--unstable` flag in the Mithril client CLI to list all available `CardanoStakeDistribution` and to download artifact by epoch or hash.
- Implement `mithril-client` library functions related to `CardanoStakeDistribution` within the WASM library.

- Crates versions:

Expand Down
96 changes: 96 additions & 0 deletions internal/mithril-build-script/src/fake_aggregator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ pub struct FakeAggregatorData {
ctx_snapshots_list: FileContent,
individual_ctx_snapshots: BTreeMap<ArtifactId, FileContent>,
ctx_proofs: BTreeMap<ArtifactId, FileContent>,

csds_list: FileContent,
individual_csds: BTreeMap<ArtifactId, FileContent>,
}

impl FakeAggregatorData {
Expand All @@ -53,6 +56,9 @@ impl FakeAggregatorData {
"snapshots-list.json" => {
data.snapshots_list = file_content;
}
"cardano-stake-distributions-list.json" => {
data.csds_list = file_content;
}
"certificates-list.json" => {
data.certificates_list = file_content;
}
Expand All @@ -65,6 +71,9 @@ impl FakeAggregatorData {
"snapshots.json" => {
data.individual_snapshots = Self::read_artifacts_json_file(&entry.path());
}
"cardano-stake-distributions.json" => {
data.individual_csds = Self::read_artifacts_json_file(&entry.path());
}
"certificates.json" => {
data.individual_certificates = Self::read_artifacts_json_file(&entry.path());
}
Expand Down Expand Up @@ -93,6 +102,14 @@ impl FakeAggregatorData {
"msd_hashes",
BTreeSet::from_iter(self.individual_msds.keys().cloned()),
),
generate_ids_array(
"csd_hashes",
BTreeSet::from_iter(self.individual_csds.keys().cloned()),
),
generate_ids_array(
"csd_epochs",
BTreeSet::from_iter(extract_csd_epochs(&self.individual_csds)),
),
generate_ids_array(
"certificate_hashes",
BTreeSet::from_iter(self.individual_certificates.keys().cloned()),
Expand Down Expand Up @@ -126,6 +143,16 @@ impl FakeAggregatorData {
),
generate_artifact_getter("msds", self.individual_msds),
generate_list_getter("msd_list", self.msds_list),
generate_ids_array(
"csd_hashes",
BTreeSet::from_iter(self.individual_csds.keys().cloned()),
),
generate_ids_array(
"csd_epochs",
BTreeSet::from_iter(extract_csd_epochs(&self.individual_csds)),
),
generate_artifact_getter("csds", self.individual_csds),
generate_list_getter("csd_list", self.csds_list),
generate_ids_array(
"certificate_hashes",
BTreeSet::from_iter(self.individual_certificates.keys().cloned()),
Expand Down Expand Up @@ -181,6 +208,28 @@ impl FakeAggregatorData {
}
}

pub fn extract_csd_epochs(individual_csds: &BTreeMap<ArtifactId, FileContent>) -> Vec<String> {
individual_csds
.values()
.map(|content| {
let json_value: serde_json::Value =
serde_json::from_str(content).unwrap_or_else(|err| {
panic!(
"Failed to parse JSON in csd content: {}\nError: {}",
content, err
);
});

json_value
.get("epoch")
.and_then(|epoch| epoch.as_u64().map(|s| s.to_string()))
.unwrap_or_else(|| {
panic!("Epoch not found or invalid in csd content: {}", content);
})
})
.collect()
}

fn extract_artifact_id_and_content(
key: &String,
value: &serde_json::Value,
Expand Down Expand Up @@ -358,4 +407,51 @@ fn b() {}
]);
assert_eq!(expected, id_per_json);
}

#[test]
fn extract_csd_epochs_with_valid_data() {
let mut csds = BTreeMap::new();
csds.insert(
"csd-123".to_string(),
r#"{"hash": "csd-123", "epoch": 123}"#.to_string(),
);
csds.insert(
"csd-456".to_string(),
r#"{"hash": "csd-456", "epoch": 456}"#.to_string(),
);

let epochs = extract_csd_epochs(&csds);

assert_eq!(epochs, vec![123.to_string(), 456.to_string()]);
}

#[test]
#[should_panic(expected = "Failed to parse JSON in csd content")]
fn extract_csd_epochs_with_invalid_json() {
let mut csds = BTreeMap::new();
csds.insert(
"csd-123".to_string(),
r#""hash": "csd-123", "epoch": "123"#.to_string(),
);

extract_csd_epochs(&csds);
}

#[test]
#[should_panic(expected = "Epoch not found or invalid in csd content")]
fn test_extract_csd_epochs_with_missing_epoch() {
let mut csds = BTreeMap::new();
csds.insert("csd-123".to_string(), r#"{"hash": "csd-123"}"#.to_string());

extract_csd_epochs(&csds);
}

#[test]
fn test_extract_csd_epochs_with_empty_map() {
let csds = BTreeMap::new();

let epochs = extract_csd_epochs(&csds);

assert!(epochs.is_empty());
}
}
167 changes: 165 additions & 2 deletions mithril-client-wasm/src/client_wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::sync::Arc;
use wasm_bindgen::prelude::*;

use mithril_client::{
common::Epoch,
feedback::{FeedbackReceiver, MithrilEvent},
CardanoTransactionsProofs, Client, ClientBuilder, MessageBuilder, MithrilCertificate,
};
Expand Down Expand Up @@ -220,6 +221,12 @@ impl MithrilUnstableClient {
}
}

/// Creates an [Epoch] instance from a [u64] value.
#[wasm_bindgen]
pub fn create_epoch(&self, value: u64) -> Epoch {
Epoch(value)
}
Alenar marked this conversation as resolved.
Show resolved Hide resolved

/// Call the client for the list of available Cardano transactions snapshots
#[wasm_bindgen]
pub async fn list_cardano_transactions_snapshots(&self) -> WasmResult {
Expand Down Expand Up @@ -292,6 +299,70 @@ impl MithrilUnstableClient {

Ok(serde_wasm_bindgen::to_value(&result)?)
}

/// Call the client to get a cardano stake distribution from a hash
#[wasm_bindgen]
pub async fn get_cardano_stake_distribution(&self, hash: &str) -> WasmResult {
let result = self
.client
.cardano_stake_distribution()
.get(hash)
.await
.map_err(|err| format!("{err:?}"))?
.ok_or(JsValue::from_str(&format!(
"No cardano stake distribution found for hash: '{hash}'"
)))?;

Ok(serde_wasm_bindgen::to_value(&result)?)
}

/// Call the client to get a cardano stake distribution from an epoch
/// The epoch represents the epoch at the end of which the Cardano stake distribution is computed by the Cardano node
#[wasm_bindgen]
pub async fn get_cardano_stake_distribution_by_epoch(&self, epoch: Epoch) -> WasmResult {
let result = self
.client
.cardano_stake_distribution()
.get_by_epoch(epoch)
.await
.map_err(|err| format!("{err:?}"))?
.ok_or(JsValue::from_str(&format!(
"No cardano stake distribution found for epoch: '{epoch}'"
)))?;

Ok(serde_wasm_bindgen::to_value(&result)?)
}

/// Call the client for the list of available cardano stake distributions
#[wasm_bindgen]
pub async fn list_cardano_stake_distributions(&self) -> WasmResult {
let result = self
.client
.cardano_stake_distribution()
.list()
.await
.map_err(|err| format!("{err:?}"))?;

Ok(serde_wasm_bindgen::to_value(&result)?)
}

/// Call the client to compute a cardano stake distribution message
#[wasm_bindgen]
pub async fn compute_cardano_stake_distribution_message(
&self,
certificate: JsValue,
cardano_stake_distribution: JsValue,
) -> WasmResult {
let certificate =
serde_wasm_bindgen::from_value(certificate).map_err(|err| format!("{err:?}"))?;
let cardano_stake_distribution = serde_wasm_bindgen::from_value(cardano_stake_distribution)
.map_err(|err| format!("{err:?}"))?;
let result = MessageBuilder::new()
.compute_cardano_stake_distribution_message(&certificate, &cardano_stake_distribution)
.map_err(|err| format!("{err:?}"))?;

Ok(serde_wasm_bindgen::to_value(&result)?)
}
}

#[cfg(test)]
Expand All @@ -301,8 +372,9 @@ mod tests {
use wasm_bindgen_test::*;

use mithril_client::{
common::ProtocolMessage, CardanoTransactionSnapshot, MithrilCertificateListItem,
MithrilStakeDistribution, MithrilStakeDistributionListItem, Snapshot, SnapshotListItem,
common::ProtocolMessage, CardanoStakeDistribution, CardanoStakeDistributionListItem,
CardanoTransactionSnapshot, MithrilCertificateListItem, MithrilStakeDistribution,
MithrilStakeDistributionListItem, Snapshot, SnapshotListItem,
};

const GENESIS_VERIFICATION_KEY: &str = "5b33322c3235332c3138362c3230312c3137372c31312c3131372c3133352c3138372c3136372c3138312c3138382c32322c35392c3230362c3130352c3233312c3135302c3231352c33302c37382c3231322c37362c31362c3235322c3138302c37322c3133342c3133372c3234372c3136312c36385d";
Expand Down Expand Up @@ -556,4 +628,95 @@ mod tests {
.await
.expect("Compute tx proof message for matching cert failed");
}

#[wasm_bindgen_test]
async fn list_cardano_stake_distributions_should_return_value_convertible_in_rust_type() {
let csd_list_js_value = get_mithril_client()
.unstable
.list_cardano_stake_distributions()
.await
.expect("cardano_mithril_stake_distributions should not fail");
let csd_list = serde_wasm_bindgen::from_value::<Vec<CardanoStakeDistributionListItem>>(
csd_list_js_value,
)
.expect("conversion should not fail");

assert_eq!(
csd_list.len(),
// Aggregator return up to 20 items for a list route
test_data::csd_hashes().len().min(20)
);
}

#[wasm_bindgen_test]
async fn get_cardano_stake_distribution_should_return_value_convertible_in_rust_type() {
let csd_js_value = get_mithril_client()
.unstable
.get_cardano_stake_distribution(test_data::csd_hashes()[0])
.await
.expect("get_cardano_stake_distribution should not fail");
let csd = serde_wasm_bindgen::from_value::<CardanoStakeDistribution>(csd_js_value)
.expect("conversion should not fail");

assert_eq!(csd.hash, test_data::csd_hashes()[0]);
}

#[wasm_bindgen_test]
async fn get_cardano_stake_distribution_should_fail_with_unknown_hash() {
get_mithril_client()
.unstable
.get_cardano_stake_distribution("whatever")
.await
.expect_err("get_cardano_stake_distribution should fail");
}

#[wasm_bindgen_test]
async fn get_cardano_stake_distribution_by_epoch_should_return_value_convertible_in_rust_type()
{
let epoch = Epoch(test_data::csd_epochs()[0].parse().unwrap());
let csd_js_value = get_mithril_client()
.unstable
.get_cardano_stake_distribution_by_epoch(epoch)
.await
.expect("get_cardano_stake_distribution_by_epoch should not fail");

let csd = serde_wasm_bindgen::from_value::<CardanoStakeDistribution>(csd_js_value)
.expect("conversion should not fail");

assert_eq!(csd.epoch, epoch);
}

#[wasm_bindgen_test]
async fn get_cardano_stake_distribution_by_epoch_should_fail_with_unknown_epoch() {
get_mithril_client()
.unstable
.get_cardano_stake_distribution_by_epoch(Epoch(u64::MAX))
.await
.expect_err("get_cardano_stake_distribution_by_epoch should fail");
}

#[wasm_bindgen_test]
async fn compute_cardano_stake_distribution_message_should_return_value_convertible_in_rust_type(
) {
let client = get_mithril_client();
let csd_js_value = client
.unstable
.get_cardano_stake_distribution(test_data::csd_hashes()[0])
.await
.unwrap();
let csd = serde_wasm_bindgen::from_value::<CardanoStakeDistribution>(csd_js_value.clone())
.unwrap();
let certificate_js_value = client
.get_mithril_certificate(&csd.certificate_hash)
.await
.unwrap();

let message_js_value = client
.unstable
.compute_cardano_stake_distribution_message(certificate_js_value, csd_js_value)
.await
.expect("compute_cardano_stake_distribution_message should not fail");
serde_wasm_bindgen::from_value::<ProtocolMessage>(message_js_value)
.expect("conversion should not fail");
}
}
Loading
Loading