mod canister;
mod icrc_analytics;
mod icrc_index;
mod metric;
mod network;
mod node_provider_reward;
mod node_status;
mod replica_version;
#[cfg(test)]
mod tests;
use crate::ic::{
IC_DASHBOARD_AUTHORITY, IC_DASHBOARD_NETWORK, IC_DASHBOARD_REPORT_SCHEMA_VERSION,
IcDashboardReportProvenance, IcHostError, IcSourceRequest,
};
use ic_platform::candid::contract::Principal;
use std::fmt::Debug;
pub use canister::{IcCanisterCollectionSource, IcCanisterSource};
pub use icrc_analytics::IcIcrcAnalyticsSource;
pub use icrc_index::IcIcrcIndexSource;
pub use metric::IcMetricSource;
pub use network::IcNetworkSource;
pub use node_provider_reward::IcNodeProviderRewardSource;
pub use node_status::IcNodeStatusSource;
pub use replica_version::IcReplicaVersionSource;
pub(super) use canister::{
canonical_canister_id, canonical_page_cursors, count_report_from_source, normalized_filters,
page_report_from_source, report_from_source, validate_page_cursor_exclusivity,
validate_page_limit,
};
pub(super) use icrc_analytics::{
icrc_indexed_count_report_from_source, icrc_token_value_report_from_source,
icrc_total_supply_report_from_source, validate_icrc_token_value_query,
validate_icrc_token_value_request, validate_icrc_total_supply_query,
validate_icrc_total_supply_request,
};
pub(super) use icrc_index::{
icrc_account_info_report_from_source, icrc_account_list_report_from_source,
icrc_holder_list_report_from_source, normalized_account_list_query, validate_account_id,
validate_holder_list_query,
};
pub(super) use metric::{
metric_report_from_source, validate_metric_query, validate_metric_request,
};
pub(super) use network::{
boundary_node_data_centers_report_from_source, daily_stats_report_from_source,
validate_daily_stats_query, validate_daily_stats_request,
};
pub(super) use node_provider_reward::{
node_provider_reward_history_report_from_source, node_provider_reward_info_report_from_source,
node_provider_reward_list_report_from_source, validate_node_provider_reward_history_query,
validate_node_provider_reward_history_request, validate_node_provider_reward_list_query,
};
pub(super) use node_status::node_status_snapshot_from_source;
pub(super) use replica_version::{
replica_version_info_report_from_source, replica_version_list_report_from_source,
validate_replica_version_id, validate_replica_version_list_query,
};
pub fn validate_dashboard_network(network: &str) -> Result<(), IcHostError> {
if network == IC_DASHBOARD_NETWORK {
return Ok(());
}
Err(IcHostError::InvalidRequest {
field: "network",
reason: format!("must be the supported mainnet identity {IC_DASHBOARD_NETWORK:?}"),
})
}
pub fn validate_source_text(
field: &'static str,
value: &str,
max_bytes: usize,
) -> Result<(), IcHostError> {
if value.is_empty()
|| value.trim() != value
|| value.len() > max_bytes
|| value.chars().any(char::is_control)
{
return invalid_source(format!(
"{field} must be nonempty trimmed text of at most {max_bytes} bytes without control characters"
));
}
Ok(())
}
pub fn validate_provenance(
expected: &IcSourceRequest,
actual: &IcSourceRequest,
) -> Result<(), IcHostError> {
if let Some(reason) = crate::report::source_provenance_mismatch(
(
&expected.endpoint,
&expected.fetched_at,
&expected.fetched_by,
),
(&actual.endpoint, &actual.fetched_at, &actual.fetched_by),
) {
return invalid_source(reason);
}
Ok(())
}
fn validate_requested_query<Query: Debug + PartialEq>(
label: &str,
expected: &Query,
actual: &Query,
) -> Result<(), IcHostError> {
if actual != expected {
return invalid_source(format!(
"{label} query is {actual:?}, expected requested query {expected:?}"
));
}
Ok(())
}
fn validate_indexed_page_bounds(
requested_maximum: Option<u64>,
resolved_maximum: u64,
maximum_field: &str,
returned_count: usize,
limit: u16,
label: &str,
) -> Result<(), IcHostError> {
if let Some(requested_maximum) = requested_maximum
&& resolved_maximum > requested_maximum
{
return invalid_source(format!(
"{maximum_field} is {resolved_maximum}, greater than requested ceiling {requested_maximum}"
));
}
if returned_count > usize::from(limit) {
return invalid_source(format!(
"source returned {returned_count} {label} rows for requested limit {limit}"
));
}
Ok(())
}
fn validate_offset_page(
offset: u64,
total: u64,
returned_count: usize,
total_field: &str,
overflow_reason: &str,
) -> Result<Option<u64>, IcHostError> {
let returned_count = u64::try_from(returned_count).unwrap_or(u64::MAX);
if returned_count > total {
return invalid_source(format!(
"returned_count {returned_count} exceeds {total_field} {total}"
));
}
if returned_count == 0 {
return Ok(None);
}
if offset >= total {
return invalid_source(format!(
"nonempty page starts at offset {offset}, but {total_field} is {total}"
));
}
let consumed = offset
.checked_add(returned_count)
.ok_or_else(|| invalid_source_value(overflow_reason))?;
if consumed > total {
return invalid_source(format!(
"page ending at offset {consumed} exceeds {total_field} {total}"
));
}
Ok((consumed < total).then_some(consumed))
}
pub fn canonical_request_principal(
field: &'static str,
value: &str,
) -> Result<String, IcHostError> {
Principal::from_text(value)
.map(|principal| principal.to_text())
.map_err(|error| IcHostError::InvalidPrincipal {
field,
reason: error.to_string(),
})
}
pub fn validate_canonical_principal(field: &'static str, value: &str) -> Result<(), IcHostError> {
crate::principal_text::validate_canonical(
value,
|reason| invalid_source_value(format!("{field} {value:?}: {reason}")),
|canonical| {
invalid_source_value(format!(
"{field} {value:?} is not canonical principal text; expected {canonical:?}"
))
},
)
}
fn validate_principal_match(
field: &'static str,
expected: &str,
actual: &str,
) -> Result<(), IcHostError> {
validate_canonical_principal(field, actual)?;
if actual == expected {
return Ok(());
}
invalid_source(format!(
"{field} is {actual:?}, expected requested principal {expected:?}"
))
}
fn validate_collection_end(now_unix_secs: u64, end_unix_secs: u64) -> Result<(), IcHostError> {
if end_unix_secs > now_unix_secs {
return invalid_request(
"query.end_unix_secs",
"must not be later than the collection time",
);
}
Ok(())
}
fn inclusive_observation_count(start_unix_secs: u64, end_unix_secs: u64, step_secs: u32) -> u64 {
(end_unix_secs - start_unix_secs) / u64::from(step_secs) + 1
}
fn validate_observation_budget(
start_unix_secs: u64,
end_unix_secs: u64,
step_secs: u32,
maximum: u64,
scope: &str,
) -> Result<(), IcHostError> {
let observations = inclusive_observation_count(start_unix_secs, end_unix_secs, step_secs);
if observations > maximum {
return invalid_request(
"query",
format!("would request {observations} observations{scope}; maximum is {maximum}"),
);
}
Ok(())
}
pub fn report_provenance(source: IcSourceRequest) -> IcDashboardReportProvenance {
IcDashboardReportProvenance {
schema_version: IC_DASHBOARD_REPORT_SCHEMA_VERSION,
network: IC_DASHBOARD_NETWORK.to_string(),
authority: IC_DASHBOARD_AUTHORITY.to_string(),
source_endpoint: source.endpoint,
fetched_at: source.fetched_at,
fetched_by: source.fetched_by,
certified: false,
point_in_time_guaranteed: false,
}
}
pub fn invalid_source<T>(reason: impl Into<String>) -> Result<T, IcHostError> {
Err(invalid_source_value(reason))
}
pub fn invalid_source_value(reason: impl Into<String>) -> IcHostError {
IcHostError::InvalidSourceData {
reason: reason.into(),
}
}
fn invalid_request<T>(field: &'static str, reason: impl Into<String>) -> Result<T, IcHostError> {
Err(IcHostError::InvalidRequest {
field,
reason: reason.into(),
})
}
fn validate_observation_timestamps(
timestamps: impl IntoIterator<Item = u64>,
window: std::ops::RangeInclusive<u64>,
label: &str,
) -> Result<(), IcHostError> {
let mut previous = None;
for timestamp in timestamps {
if !window.contains(×tamp) {
return invalid_source(format!(
"{label} observation timestamp {timestamp} is outside the requested window"
));
}
if previous.is_some_and(|previous| previous >= timestamp) {
return invalid_source(format!(
"{label} observations must be strictly ordered by timestamp"
));
}
previous = Some(timestamp);
}
Ok(())
}
fn validate_query_time_order(start: u64, end: u64) -> Result<(), IcHostError> {
if end < start {
return invalid_request(
"query.end_unix_secs",
"must be greater than or equal to query.start_unix_secs",
);
}
Ok(())
}