ic-query 0.89.0

Internet Computer query library for CloudEngine, NNS, SNS, ICRC, system canisters, and public network metadata
Documentation
//! Module: ic::source
//!
//! Responsibility: shared Dashboard source provenance and capability facade.
//! Does not own: HTTP transport, capability-specific validation, command parsing, or rendering.
//! Boundary: treats live and custom source provenance as untrusted authority data.

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(),
    })
}

/// Validate the common ordering and window contract of Dashboard observations.
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(&timestamp) {
            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(())
}