ic-query 0.25.12

Internet Computer query library for NNS, SNS, ICRC, system canisters, and public network metadata
Documentation
use super::{
    DEFAULT_NNS_NEURON_SOURCE_ENDPOINT, NnsKnownNeuronData, NnsNeuronHostError,
    NnsNeuronInfoRequest, NnsNeuronListRequest, NnsNeuronPage, NnsNeuronRow, NnsNeuronSource,
    build_nns_neuron_cache_status_report, build_nns_neuron_info_report_from_cache,
    build_nns_neuron_info_report_with_source, build_nns_neuron_list_report_from_cache,
    build_nns_neuron_list_report_with_source, nns_neuron_cache_path,
    refresh_nns_neuron_cache_with_source,
};
use crate::{
    nns::{
        LiveNnsSource, NnsGovernanceCacheRequest, NnsGovernanceRefreshRequest, NnsSourceRequest,
    },
    subnet_catalog::MAINNET_NETWORK,
    test_support::temp_dir,
};
use std::{
    fs,
    sync::atomic::{AtomicUsize, Ordering},
};

static LIVE_SOURCE_CALLS: AtomicUsize = AtomicUsize::new(0);

struct FixtureSource;

impl NnsNeuronSource for FixtureSource {
    fn fetch_neuron_page(
        &self,
        request: &NnsSourceRequest,
        exclusive_start_neuron_id: Option<u64>,
        page_size: u32,
    ) -> Result<NnsNeuronPage, NnsNeuronHostError> {
        assert_eq!(request.network, MAINNET_NETWORK);
        assert_eq!(request.endpoint, DEFAULT_NNS_NEURON_SOURCE_ENDPOINT);
        assert_eq!(page_size, 2);
        let (neurons, next_start_neuron_id) = match exclusive_start_neuron_id {
            None => (vec![sample_neuron(1), sample_neuron(2)], Some(2)),
            Some(2) => (vec![sample_neuron(3)], None),
            other => panic!("unexpected neuron cursor: {other:?}"),
        };
        Ok(NnsNeuronPage {
            neurons,
            next_start_neuron_id,
        })
    }

    fn fetch_neuron(
        &self,
        request: &NnsSourceRequest,
        neuron_id: u64,
    ) -> Result<NnsNeuronRow, NnsNeuronHostError> {
        assert_eq!(request.network, MAINNET_NETWORK);
        Ok(sample_neuron(neuron_id))
    }
}

struct CountingSource;

impl NnsNeuronSource for CountingSource {
    fn fetch_neuron_page(
        &self,
        _request: &NnsSourceRequest,
        _exclusive_start_neuron_id: Option<u64>,
        _page_size: u32,
    ) -> Result<NnsNeuronPage, NnsNeuronHostError> {
        LIVE_SOURCE_CALLS.fetch_add(1, Ordering::SeqCst);
        unreachable!("unsupported network must be rejected before source invocation")
    }

    fn fetch_neuron(
        &self,
        _request: &NnsSourceRequest,
        _neuron_id: u64,
    ) -> Result<NnsNeuronRow, NnsNeuronHostError> {
        LIVE_SOURCE_CALLS.fetch_add(1, Ordering::SeqCst);
        unreachable!("unsupported network must be rejected before source invocation")
    }
}

#[test]
fn public_list_and_info_reports_preserve_governance_rows() {
    let list_request = NnsNeuronListRequest::new(
        MAINNET_NETWORK,
        DEFAULT_NNS_NEURON_SOURCE_ENDPOINT,
        1_700_000_000,
        2,
    );
    let list = build_nns_neuron_list_report_with_source(&list_request, &FixtureSource)
        .expect("live list report");

    assert!(!list.from_cache);
    assert_eq!(list.next_start_neuron_id, Some(2));
    assert_eq!(
        list.neurons
            .iter()
            .map(|row| row.neuron_id)
            .collect::<Vec<_>>(),
        vec![1, 2]
    );
    assert_eq!(list.neurons[0].visibility, Some(2));
    assert_eq!(list.neurons[0].visibility_text, "public");

    let info_request = NnsNeuronInfoRequest::new(
        MAINNET_NETWORK,
        DEFAULT_NNS_NEURON_SOURCE_ENDPOINT,
        1_700_000_000,
        42,
    );
    let info = build_nns_neuron_info_report_with_source(&info_request, &FixtureSource)
        .expect("live info report");

    assert!(!info.from_cache);
    assert_eq!(info.neuron.neuron_id, 42);
    assert_eq!(
        info.neuron
            .known_neuron_data
            .as_ref()
            .expect("known neuron")
            .name,
        "Neuron 42"
    );
}

#[test]
fn public_builders_reject_non_mainnet_before_invoking_a_source() {
    LIVE_SOURCE_CALLS.store(0, Ordering::SeqCst);
    let list_request = NnsNeuronListRequest::new("local", "http://127.0.0.1:1", 1_700_000_000, 2);
    let list_error = build_nns_neuron_list_report_with_source(&list_request, &CountingSource)
        .expect_err("non-mainnet list must fail");
    assert!(matches!(
        list_error,
        NnsNeuronHostError::UnsupportedNetwork { ref network } if network == "local"
    ));

    let info_request = NnsNeuronInfoRequest::new("local", "http://127.0.0.1:1", 1_700_000_000, 1);
    let info_error = build_nns_neuron_info_report_with_source(&info_request, &CountingSource)
        .expect_err("non-mainnet info must fail");
    assert!(matches!(
        info_error,
        NnsNeuronHostError::UnsupportedNetwork { ref network } if network == "local"
    ));
    assert_eq!(LIVE_SOURCE_CALLS.load(Ordering::SeqCst), 0);
}

#[test]
fn live_source_rejects_non_mainnet_before_agent_construction() {
    let request = NnsSourceRequest::new(
        "local",
        "not a valid replica endpoint",
        "2023-11-14T22:13:20Z",
        "test",
    );

    let error = LiveNnsSource
        .fetch_neuron_page(&request, None, 2)
        .expect_err("live source must reject non-mainnet");

    assert!(matches!(
        error,
        NnsNeuronHostError::UnsupportedNetwork { ref network } if network == "local"
    ));
}

#[test]
fn list_rejects_a_cursor_that_does_not_match_the_page() {
    struct InvalidCursorSource;

    impl NnsNeuronSource for InvalidCursorSource {
        fn fetch_neuron_page(
            &self,
            _request: &NnsSourceRequest,
            _exclusive_start_neuron_id: Option<u64>,
            _page_size: u32,
        ) -> Result<NnsNeuronPage, NnsNeuronHostError> {
            Ok(NnsNeuronPage {
                neurons: vec![sample_neuron(1), sample_neuron(2)],
                next_start_neuron_id: Some(1),
            })
        }

        fn fetch_neuron(
            &self,
            _request: &NnsSourceRequest,
            neuron_id: u64,
        ) -> Result<NnsNeuronRow, NnsNeuronHostError> {
            Ok(sample_neuron(neuron_id))
        }
    }

    let request = NnsNeuronListRequest::new(
        MAINNET_NETWORK,
        DEFAULT_NNS_NEURON_SOURCE_ENDPOINT,
        1_700_000_000,
        2,
    );
    let error = build_nns_neuron_list_report_with_source(&request, &InvalidCursorSource)
        .expect_err("invalid cursor must fail");

    assert!(matches!(error, NnsNeuronHostError::InvalidPage { .. }));
}

#[test]
fn refresh_publishes_one_complete_snapshot_for_cached_list_and_info() {
    let root = temp_dir("ic-query-nns-neuron-cache");
    let refresh_request = NnsGovernanceRefreshRequest::new(
        &root,
        MAINNET_NETWORK,
        DEFAULT_NNS_NEURON_SOURCE_ENDPOINT,
        1_700_000_000,
        2,
    );

    let refresh = refresh_nns_neuron_cache_with_source(&refresh_request, &FixtureSource)
        .expect("refresh complete neuron cache");

    assert_eq!(refresh.neuron_count, 3);
    assert_eq!(refresh.page_count, 2);
    assert!(refresh.complete);
    assert!(!refresh.point_in_time_guaranteed);
    let path = nns_neuron_cache_path(&root, MAINNET_NETWORK);
    assert!(path.is_file());
    let cache: serde_json::Value =
        serde_json::from_slice(&fs::read(&path).expect("read neuron cache"))
            .expect("parse neuron cache");
    assert_eq!(cache["collection"], "neurons");
    assert_eq!(cache["completeness"]["status"], "api_exhausted");
    assert_eq!(cache["completeness"]["point_in_time_guaranteed"], false);

    let list_request = NnsNeuronListRequest::new(
        MAINNET_NETWORK,
        DEFAULT_NNS_NEURON_SOURCE_ENDPOINT,
        1_700_000_001,
        2,
    )
    .with_exclusive_start_neuron_id(1);
    let list = build_nns_neuron_list_report_from_cache(&list_request, &root)
        .expect("load cached list")
        .expect("complete cache");
    assert!(list.from_cache);
    assert_eq!(list.total_neuron_count, Some(3));
    assert!(!list.point_in_time_guaranteed);
    assert_eq!(
        list.neurons
            .iter()
            .map(|row| row.neuron_id)
            .collect::<Vec<_>>(),
        vec![2, 3]
    );
    assert_eq!(list.next_start_neuron_id, None);

    let info_request = NnsNeuronInfoRequest::new(
        MAINNET_NETWORK,
        DEFAULT_NNS_NEURON_SOURCE_ENDPOINT,
        1_700_000_001,
        2,
    );
    let info = build_nns_neuron_info_report_from_cache(&info_request, &root)
        .expect("load cached detail")
        .expect("cached neuron");
    assert!(info.from_cache);
    assert_eq!(info.neuron.neuron_id, 2);

    let status = build_nns_neuron_cache_status_report(&NnsGovernanceCacheRequest::new(
        &root,
        MAINNET_NETWORK,
    ))
    .expect("cache status");
    assert!(status.found);
    assert_eq!(
        status.cache.as_ref().expect("cache summary").cache_status,
        "ok"
    );
    assert_eq!(
        status.latest_attempt.as_ref().expect("attempt").status,
        "complete"
    );
    let _ = fs::remove_dir_all(root);
}

#[test]
fn capped_refresh_keeps_failure_evidence_without_publishing_a_snapshot() {
    let root = temp_dir("ic-query-nns-neuron-incomplete");
    let request = NnsGovernanceRefreshRequest::new(
        &root,
        MAINNET_NETWORK,
        DEFAULT_NNS_NEURON_SOURCE_ENDPOINT,
        1_700_000_000,
        2,
    )
    .with_max_pages(Some(1));

    let error = refresh_nns_neuron_cache_with_source(&request, &FixtureSource)
        .expect_err("capped refresh must remain incomplete");

    assert!(matches!(
        error,
        NnsNeuronHostError::IncompleteRefresh {
            pages_fetched: 1,
            rows_fetched: 2,
            ..
        }
    ));
    assert!(!nns_neuron_cache_path(&root, MAINNET_NETWORK).exists());
    let status = build_nns_neuron_cache_status_report(&NnsGovernanceCacheRequest::new(
        &root,
        MAINNET_NETWORK,
    ))
    .expect("cache status");
    assert!(!status.found);
    let attempt = status.latest_attempt.expect("failed attempt");
    assert_eq!(attempt.status, "failed");
    assert_eq!(attempt.pages_fetched, 1);
    assert_eq!(attempt.rows_fetched, 2);
    assert_eq!(attempt.last_cursor.as_deref(), Some("2"));
    let _ = fs::remove_dir_all(root);
}

fn sample_neuron(neuron_id: u64) -> NnsNeuronRow {
    NnsNeuronRow {
        neuron_id,
        state: 1,
        state_text: "not-dissolving".to_string(),
        visibility: Some(2),
        visibility_text: "public".to_string(),
        neuron_type: None,
        neuron_type_text: "unknown".to_string(),
        stake_e8s: neuron_id.saturating_mul(100_000_000),
        staked_maturity_e8s_equivalent: Some(10),
        dissolve_delay_seconds: 31_536_000,
        age_seconds: 86_400,
        created_timestamp_seconds: 1_600_000_000,
        retrieved_at_timestamp_seconds: 1_700_000_000,
        voting_power: neuron_id.saturating_mul(200_000_000),
        deciding_voting_power: Some(neuron_id.saturating_mul(150_000_000)),
        potential_voting_power: Some(neuron_id.saturating_mul(200_000_000)),
        voting_power_refreshed_timestamp_seconds: Some(1_699_999_000),
        joined_community_fund_timestamp_seconds: None,
        eight_year_gang_bonus_base_e8s: None,
        known_neuron_data: Some(NnsKnownNeuronData {
            name: format!("Neuron {neuron_id}"),
            description: Some("fixture neuron".to_string()),
            links: vec!["https://example.com".to_string()],
        }),
        recent_ballots: Vec::new(),
    }
}