ic-query 0.81.1

Internet Computer query library for CloudEngine, NNS, SNS, ICRC, system canisters, and public network metadata
Documentation
//! Module: nns::governance::collection
//!
//! Responsibility: shared host contracts and attempt IO for NNS Governance collections.
//! Does not own: proposal or neuron paging, cache paths, or report rendering.
//! Boundary: centralizes shared DTOs plus attempt construction and validation.

use ic_host_text::timestamp::format_utc_timestamp_secs;
mod cache;
pub use cache::{NnsGovernanceCacheStatusReport, NnsGovernanceCacheSummary};
pub(in crate::nns) use cache::{
    build_governance_cache_status_report, validate_governance_cache_completeness,
    validate_governance_snapshot,
};

mod refresh;
pub(in crate::nns) use refresh::run_governance_snapshot_refresh;

use crate::cache_file::write_managed_json_pretty_atomically;
use crate::{
    HostCacheError,
    cache::CacheRefreshAttemptStatus,
    nns::MAINNET_GOVERNANCE_CANISTER_ID,
    snapshot_cache::{
        SNAPSHOT_REFRESH_ATTEMPT_SCHEMA_VERSION, SnapshotEnvelope, SnapshotJsonPaths,
        SnapshotRefreshAttempt, SnapshotRefreshProgress, current_attempt_timestamp,
        read_snapshot_refresh_attempt_strict, validate_snapshot_refresh_attempt,
    },
};
use serde::{Deserialize as SerdeDeserialize, Serialize};
use std::path::{Path, PathBuf};

///
/// NnsGovernanceRefreshRequest
///
/// Shared request for one complete NNS Governance collection refresh.
///

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NnsGovernanceRefreshRequest {
    /// Root directory containing shared caches.
    pub cache_root: PathBuf,
    /// Requested network identity.
    pub network: String,
    /// Replica endpoint used for every page.
    pub source_endpoint: String,
    /// Caller-provided collection time in Unix seconds.
    pub now_unix_secs: u64,
    /// Governance page size.
    pub page_size: u32,
    /// Optional diagnostic page cap.
    pub max_pages: Option<u32>,
}

impl NnsGovernanceRefreshRequest {
    /// Construct a complete NNS Governance collection refresh request.
    #[must_use]
    pub fn new(
        cache_root: impl Into<PathBuf>,
        network: impl Into<String>,
        source_endpoint: impl Into<String>,
        now_unix_secs: u64,
        page_size: u32,
    ) -> Self {
        Self {
            cache_root: cache_root.into(),
            network: network.into(),
            source_endpoint: source_endpoint.into(),
            now_unix_secs,
            page_size,
            max_pages: None,
        }
    }

    /// Stop diagnostically before fetching more than the given pages.
    #[must_use]
    pub const fn with_max_pages(mut self, max_pages: Option<u32>) -> Self {
        self.max_pages = max_pages;
        self
    }
}

///
/// NnsGovernanceCacheRequest
///
/// Shared local cache scope for complete NNS Governance collections.
///

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NnsGovernanceCacheRequest {
    /// Root directory containing shared caches.
    pub cache_root: PathBuf,
    /// Requested network identity.
    pub network: String,
}

impl NnsGovernanceCacheRequest {
    /// Construct a local NNS Governance cache request.
    #[must_use]
    pub fn new(cache_root: impl Into<PathBuf>, network: impl Into<String>) -> Self {
        Self {
            cache_root: cache_root.into(),
            network: network.into(),
        }
    }

    /// Return the shared cache root.
    #[must_use]
    pub fn cache_root(&self) -> &Path {
        &self.cache_root
    }
}

///
/// NnsGovernanceRefreshReport
///
/// Serializable outcome of a complete NNS Governance collection refresh.
///

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[expect(
    clippy::struct_excessive_bools,
    reason = "Independent snapshot assurance and publication observations remain explicit in raw JSON."
)]
pub struct NnsGovernanceRefreshReport {
    /// Report schema version.
    pub schema_version: u32,
    /// Refreshed network identity.
    pub network: String,
    /// NNS Governance canister principal.
    pub governance_canister_id: String,
    /// Number of rows in the published collection.
    pub row_count: usize,
    /// Published collection, either neurons or proposals.
    pub collection: String,
    /// Whether the complete snapshot was published.
    pub wrote_cache: bool,
    /// Page size used for the walk.
    pub page_size: u32,
    /// Pages fetched through API exhaustion.
    pub page_count: u32,
    /// Whether the published collection is complete.
    pub complete: bool,
    /// Whether every row is guaranteed to describe one Governance instant.
    pub point_in_time_guaranteed: bool,
    /// Whether a previous complete cache was replaced.
    pub replaced_existing_cache: bool,
    /// Failure to finalize attempt metadata after successful publication.
    pub attempt_finalization_error: Option<String>,
    /// UTC collection timestamp.
    pub fetched_at: String,
    /// Replica endpoint used for every page.
    pub source_endpoint: String,
    /// Collector identity.
    pub fetched_by: String,
    /// Published snapshot path.
    pub cache_path: String,
    /// Refresh-attempt sidecar path.
    pub refresh_attempt_path: String,
    /// Refresh-lock path.
    pub refresh_lock_path: String,
}

impl NnsGovernanceRefreshReport {
    /// Project the snapshot only after its publication succeeded.
    pub(in crate::nns) fn from_snapshot<Rows>(
        cache: &SnapshotEnvelope<NnsGovernanceCacheMetadata, Rows>,
        paths: &SnapshotJsonPaths,
        replaced_existing_cache: bool,
        attempt_finalization_error: Option<String>,
    ) -> Self {
        Self {
            schema_version: 1,
            network: cache.network.clone(),
            governance_canister_id: cache.metadata.governance_canister_id.clone(),
            row_count: cache.completeness.row_count,
            collection: cache.collection.clone(),
            page_size: cache.completeness.page_size,
            page_count: cache.completeness.page_count,
            complete: cache.completeness.is_api_exhausted(),
            point_in_time_guaranteed: cache.completeness.point_in_time_guaranteed,
            wrote_cache: true,
            replaced_existing_cache,
            attempt_finalization_error,
            fetched_at: cache.fetched_at.clone(),
            source_endpoint: cache.source_endpoint.clone(),
            fetched_by: cache.fetched_by.clone(),
            cache_path: paths.snapshot_path.display().to_string(),
            refresh_attempt_path: paths.refresh_attempt_path.display().to_string(),
            refresh_lock_path: paths.refresh_lock_path.display().to_string(),
        }
    }
}

///
/// NnsGovernanceCacheMetadata
///
/// Shared flattened provenance for an NNS Governance snapshot or attempt.
///

#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub(in crate::nns) struct NnsGovernanceCacheMetadata {
    /// NNS Governance canister principal.
    pub(in crate::nns) governance_canister_id: String,
}

///
/// NnsGovernanceAttemptReadError
///
/// Shared internal failure from reading or validating Governance attempt evidence.
///

#[derive(Debug)]
pub(in crate::nns) enum NnsGovernanceAttemptReadError {
    /// Cache-file read or JSON parsing failed.
    Cache(HostCacheError),
    /// The sidecar did not describe the requested Governance collection.
    Invalid {
        /// Attempt sidecar path.
        path: PathBuf,
        /// Deterministic validation failure.
        reason: String,
    },
}

/// Construct canonical mainnet NNS Governance cache metadata.
#[must_use]
pub(in crate::nns) fn mainnet_governance_cache_metadata() -> NnsGovernanceCacheMetadata {
    NnsGovernanceCacheMetadata {
        governance_canister_id: MAINNET_GOVERNANCE_CANISTER_ID.to_string(),
    }
}

/// Validate the Governance canister identity in shared cache metadata.
pub(in crate::nns) fn validate_governance_cache_metadata(
    metadata: &NnsGovernanceCacheMetadata,
) -> Result<(), String> {
    if metadata.governance_canister_id == MAINNET_GOVERNANCE_CANISTER_ID {
        return Ok(());
    }
    Err(format!(
        "governance_canister_id is {}, expected {MAINNET_GOVERNANCE_CANISTER_ID}",
        metadata.governance_canister_id
    ))
}

/// Strictly read and validate one NNS Governance refresh-attempt sidecar.
pub(in crate::nns) fn read_governance_refresh_attempt(
    cache_root: &Path,
    path: &Path,
    expected_network: &str,
    cache_component: &'static str,
) -> Result<Option<SnapshotRefreshAttempt<NnsGovernanceCacheMetadata>>, NnsGovernanceAttemptReadError>
{
    let attempt = read_snapshot_refresh_attempt_strict::<
        SnapshotRefreshAttempt<NnsGovernanceCacheMetadata>,
    >(cache_root, path, None)
    .map_err(|error| {
        error.into_error(
            cache_component,
            NnsGovernanceAttemptReadError::Cache,
            |path, reason| NnsGovernanceAttemptReadError::Invalid { path, reason },
        )
    })?;
    attempt
        .map(|attempt| {
            let invalid = |reason| NnsGovernanceAttemptReadError::Invalid {
                path: path.to_path_buf(),
                reason,
            };
            validate_snapshot_refresh_attempt(&attempt, expected_network).map_err(invalid)?;
            validate_governance_cache_metadata(&attempt.metadata).map_err(invalid)?;
            Ok(attempt)
        })
        .transpose()
}

/// Strictly read one report-safe NNS Governance refresh-attempt status.
pub(in crate::nns) fn read_governance_refresh_attempt_status(
    cache_root: &Path,
    path: &Path,
    expected_network: &str,
    cache_component: &'static str,
) -> Result<Option<crate::cache::SnapshotRefreshAttemptStatus>, NnsGovernanceAttemptReadError> {
    read_governance_refresh_attempt(cache_root, path, expected_network, cache_component)
        .map(|attempt| attempt.map(Into::into))
}

/// Construct and write one validated-shape NNS Governance refresh-attempt sidecar.
fn write_governance_refresh_attempt(
    path: &Path,
    request: &NnsGovernanceRefreshRequest,
    cache_component: &'static str,
    status: CacheRefreshAttemptStatus,
    progress: SnapshotRefreshProgress,
    last_error: Option<String>,
) -> Result<(), HostCacheError> {
    let started_at = format_utc_timestamp_secs(request.now_unix_secs);
    let attempt = SnapshotRefreshAttempt {
        schema_version: SNAPSHOT_REFRESH_ATTEMPT_SCHEMA_VERSION,
        network: request.network.clone(),
        source_endpoint: request.source_endpoint.clone(),
        started_at: started_at.clone(),
        updated_at: current_attempt_timestamp(&started_at),
        metadata: mainnet_governance_cache_metadata(),
        status,
        page_size: request.page_size,
        pages_fetched: progress.pages_fetched,
        rows_fetched: progress.rows_fetched,
        last_cursor: progress.last_cursor,
        last_error,
    };
    write_managed_json_pretty_atomically(
        &request.cache_root,
        path,
        &attempt,
        crate::cache_file::MAX_REFRESH_ATTEMPT_BYTES,
        |path, source| HostCacheError::serialize_cache(cache_component, path, source),
        |error| HostCacheError::operation(cache_component, error),
    )
}

/// Write the initial running state for an NNS Governance refresh.
pub(in crate::nns) fn write_starting_governance_refresh_attempt(
    path: &Path,
    request: &NnsGovernanceRefreshRequest,
    cache_component: &'static str,
) -> Result<(), HostCacheError> {
    write_governance_refresh_attempt(
        path,
        request,
        cache_component,
        CacheRefreshAttemptStatus::Running,
        SnapshotRefreshProgress::default(),
        None,
    )
}

/// Write the latest retained progress for an NNS Governance refresh.
pub(in crate::nns) fn write_running_governance_refresh_attempt(
    path: &Path,
    request: &NnsGovernanceRefreshRequest,
    cache_component: &'static str,
    progress: SnapshotRefreshProgress,
) -> Result<(), HostCacheError> {
    write_governance_refresh_attempt(
        path,
        request,
        cache_component,
        CacheRefreshAttemptStatus::Running,
        progress,
        None,
    )
}

/// Write the terminal complete state for an NNS Governance refresh.
pub(in crate::nns) fn write_complete_governance_refresh_attempt(
    path: &Path,
    request: &NnsGovernanceRefreshRequest,
    cache_component: &'static str,
    progress: SnapshotRefreshProgress,
) -> Result<(), HostCacheError> {
    write_governance_refresh_attempt(
        path,
        request,
        cache_component,
        CacheRefreshAttemptStatus::Complete,
        progress,
        None,
    )
}

/// Preserve valid progress and write failed NNS Governance refresh evidence.
pub(in crate::nns) fn write_failed_governance_refresh_attempt(
    path: &Path,
    request: &NnsGovernanceRefreshRequest,
    cache_component: &'static str,
    last_error: String,
) -> Result<(), HostCacheError> {
    let progress = read_governance_refresh_attempt(
        &request.cache_root,
        path,
        &request.network,
        cache_component,
    )
    .ok()
    .flatten()
    .map(SnapshotRefreshProgress::from)
    .unwrap_or_default();
    write_governance_refresh_attempt(
        path,
        request,
        cache_component,
        CacheRefreshAttemptStatus::Failed,
        progress,
        Some(last_error),
    )
}