Skip to main content

ic_query/nns/neuron/report/distribution/
mod.rs

1//! Module: nns::neuron::report::distribution
2//!
3//! Responsibility: validate and aggregate complete caller-retained public NNS neuron collections.
4//! Does not own: collection transport, persistence, cache policy, or process output.
5//! Boundary: projects complete public `NeuronInfo` evidence into portable distributions.
6
7use super::{
8    NnsNeuronCollectionState,
9    classification::{NnsNeuronState, NnsNeuronType, NnsNeuronVisibility},
10    collection::validate_collection_state,
11    model::NnsNeuronRow,
12    source::validate_neuron_rows,
13};
14use crate::{
15    nns::{
16        MAINNET_GOVERNANCE_CANISTER_ID,
17        governance::{
18            NnsGovernanceCollectionStatus, NnsGovernanceSourceProvenance,
19            validate_governance_report_source, validate_governance_time_interval,
20        },
21    },
22    subnet_catalog::MAINNET_NETWORK,
23};
24use serde::{Deserialize, Serialize};
25use std::collections::BTreeMap;
26use thiserror::Error as ThisError;
27
28/// Version of the portable NNS public-neuron distribution report schema.
29pub const NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION: u32 = 1;
30
31///
32/// NnsNeuronStateDistribution
33///
34/// Public neuron count and effective stake for one raw Governance state code.
35///
36
37#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
38pub struct NnsNeuronStateDistribution {
39    /// Raw native Governance state code.
40    pub state: i32,
41    /// Classification derived from the raw state code.
42    pub state_text: NnsNeuronState,
43    /// Number of collected neurons with this state code.
44    pub neuron_count: u64,
45    /// Sum of public effective stake for neurons with this state code.
46    pub effective_stake_e8s: u64,
47}
48
49///
50/// NnsNeuronVisibilityDistribution
51///
52/// Public neuron count and effective stake for one optional Governance visibility code.
53///
54
55#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
56pub struct NnsNeuronVisibilityDistribution {
57    /// Raw optional native Governance visibility code.
58    pub visibility: Option<i32>,
59    /// Classification derived from the raw optional visibility code.
60    pub visibility_text: NnsNeuronVisibility,
61    /// Number of collected neurons with this visibility code or absence.
62    pub neuron_count: u64,
63    /// Sum of public effective stake for neurons with this visibility code or absence.
64    pub effective_stake_e8s: u64,
65}
66
67///
68/// NnsNeuronTypeDistribution
69///
70/// Public neuron count and effective stake for one optional Governance neuron-type code.
71///
72
73#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
74pub struct NnsNeuronTypeDistribution {
75    /// Raw optional native Governance neuron-type code.
76    pub neuron_type: Option<i32>,
77    /// Classification derived from the raw optional neuron-type code.
78    pub neuron_type_text: NnsNeuronType,
79    /// Number of collected neurons with this type code or absence.
80    pub neuron_count: u64,
81    /// Sum of public effective stake for neurons with this type code or absence.
82    pub effective_stake_e8s: u64,
83}
84
85///
86/// NnsNeuronDistributionReport
87///
88/// Deterministic local distribution over one complete public NNS neuron collection.
89///
90
91#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
92pub struct NnsNeuronDistributionReport {
93    /// Report schema version.
94    pub schema_version: u32,
95    /// Network identity retained by the complete collection.
96    pub network: String,
97    /// Fixed NNS Governance canister principal retained by the collection.
98    pub governance_canister_id: String,
99    /// Concrete transport and collector provenance retained by the collection.
100    pub source: NnsGovernanceSourceProvenance,
101    /// Caller-supplied time attached to collection creation.
102    pub collection_started_at: String,
103    /// Caller-supplied time attached to the final admitted page.
104    pub collection_updated_at: String,
105    /// Number of admitted source pages in the complete collection.
106    pub collection_page_count: u32,
107    /// Number of admitted public neuron rows in the complete collection.
108    pub collected_neuron_count: u64,
109    /// Whether the sequential collection is guaranteed to represent one point in time.
110    pub point_in_time_guaranteed: bool,
111    /// Earliest raw Governance retrieval timestamp among collected rows.
112    pub earliest_retrieved_at_timestamp_seconds: Option<u64>,
113    /// Latest raw Governance retrieval timestamp among collected rows.
114    pub latest_retrieved_at_timestamp_seconds: Option<u64>,
115    /// Sum of public effective stake across every collected neuron.
116    pub total_effective_stake_e8s: u64,
117    /// Rows carrying a public staked-maturity value.
118    pub reported_staked_maturity_neuron_count: u64,
119    /// Rows without a public staked-maturity value.
120    pub unreported_staked_maturity_neuron_count: u64,
121    /// Sum of reported staked maturity values only.
122    pub total_reported_staked_maturity_e8s_equivalent: u64,
123    /// Rows carrying current deciding voting power.
124    pub reported_deciding_voting_power_neuron_count: u64,
125    /// Rows without current deciding voting power.
126    pub unreported_deciding_voting_power_neuron_count: u64,
127    /// Sum of reported current deciding voting power values only.
128    pub total_reported_deciding_voting_power: u64,
129    /// Rows carrying current potential voting power.
130    pub reported_potential_voting_power_neuron_count: u64,
131    /// Rows without current potential voting power.
132    pub unreported_potential_voting_power_neuron_count: u64,
133    /// Sum of reported current potential voting power values only.
134    pub total_reported_potential_voting_power: u64,
135    /// Rows carrying registered known-neuron metadata.
136    pub known_neuron_metadata_count: u64,
137    /// Rows carrying a public Neurons' Fund join timestamp.
138    pub neurons_fund_join_timestamp_present_count: u64,
139    /// Canonically raw-code-ordered distribution by neuron state.
140    pub state_distribution: Vec<NnsNeuronStateDistribution>,
141    /// Canonically optional-raw-code-ordered distribution by visibility.
142    pub visibility_distribution: Vec<NnsNeuronVisibilityDistribution>,
143    /// Canonically optional-raw-code-ordered distribution by neuron type.
144    pub neuron_type_distribution: Vec<NnsNeuronTypeDistribution>,
145}
146
147///
148/// NnsNeuronDistributionValidationError
149///
150/// Pure validation failure for an untrusted serialized or in-memory distribution report.
151///
152
153#[derive(Debug, Eq, PartialEq, ThisError)]
154#[error("invalid NNS neuron distribution report: {reason}")]
155pub struct NnsNeuronDistributionValidationError {
156    /// Deterministic invariant failure.
157    pub reason: String,
158}
159
160///
161/// NnsNeuronDistributionError
162///
163/// Deterministic validation or accounting failure from local neuron distribution projection.
164///
165
166#[derive(Debug, ThisError)]
167pub enum NnsNeuronDistributionError {
168    /// The supplied collection state failed its shared continuation invariants.
169    #[error("invalid NNS neuron collection state for distribution projection: {reason}")]
170    InvalidCollectionState {
171        /// Deterministic collection invariant failure.
172        reason: String,
173    },
174
175    /// The collection stopped without observing Governance API exhaustion.
176    #[error("NNS neuron distribution requires a complete collection; state is {status}")]
177    CollectionNotComplete {
178        /// Current lifecycle of the otherwise valid collection state.
179        status: NnsGovernanceCollectionStatus,
180    },
181
182    /// The supplied rows do not match the collection's admitted-row accounting.
183    #[error(
184        "NNS neuron distribution received {actual} rows; complete collection accounts for {expected}"
185    )]
186    NeuronCountMismatch {
187        /// Neuron rows accounted for by the collection state.
188        expected: u64,
189        /// Neuron rows supplied to the builder.
190        actual: u64,
191    },
192
193    /// Supplied rows failed the shared public-neuron response contract.
194    #[error("invalid NNS neuron rows for distribution projection: {reason}")]
195    InvalidNeuronRows {
196        /// Deterministic row invariant failure.
197        reason: String,
198    },
199
200    /// A count conversion, increment, or numeric sum exceeded `u64`.
201    #[error("NNS neuron distribution accounting overflow while updating {field}")]
202    AccountingOverflow {
203        /// Count or sum that exceeded its representation.
204        field: &'static str,
205    },
206}
207
208/// Build one deterministic distribution from a complete caller-retained public-neuron collection.
209pub fn build_nns_neuron_distribution_report(
210    collection: &NnsNeuronCollectionState,
211    neurons: &[NnsNeuronRow],
212) -> Result<NnsNeuronDistributionReport, NnsNeuronDistributionError> {
213    validate_collection_state(collection).map_err(|error| {
214        NnsNeuronDistributionError::InvalidCollectionState {
215            reason: error.to_string(),
216        }
217    })?;
218    if !collection.is_complete() {
219        return Err(NnsNeuronDistributionError::CollectionNotComplete {
220            status: collection.status(),
221        });
222    }
223    let source = collection.source().cloned().ok_or_else(|| {
224        NnsNeuronDistributionError::InvalidCollectionState {
225            reason: "complete collection has no concrete source provenance".to_string(),
226        }
227    })?;
228
229    let expected = collection.neurons_fetched();
230    let actual = u64::try_from(neurons.len()).map_err(|_| {
231        NnsNeuronDistributionError::AccountingOverflow {
232            field: "supplied_neuron_count",
233        }
234    })?;
235    if actual != expected {
236        return Err(NnsNeuronDistributionError::NeuronCountMismatch { expected, actual });
237    }
238    validate_neuron_rows(neurons).map_err(|error| {
239        NnsNeuronDistributionError::InvalidNeuronRows {
240            reason: error.to_string(),
241        }
242    })?;
243
244    let mut distribution = DistributionAccumulator::default();
245    for neuron in neurons {
246        distribution.observe(neuron)?;
247    }
248    Ok(distribution.into_report(collection, expected, source))
249}
250
251/// Validate every distribution-report invariant available without source rows or live host calls.
252pub fn validate_nns_neuron_distribution_report(
253    report: &NnsNeuronDistributionReport,
254) -> Result<(), NnsNeuronDistributionValidationError> {
255    validate_distribution_header(report)?;
256    validate_distribution_summary(report)?;
257    validate_state_distribution(report)?;
258    validate_visibility_distribution(report)?;
259    validate_neuron_type_distribution(report)
260}
261
262fn validate_distribution_header(
263    report: &NnsNeuronDistributionReport,
264) -> Result<(), NnsNeuronDistributionValidationError> {
265    if report.schema_version != NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION {
266        return Err(invalid_validation(format!(
267            "schema version {} does not equal {}",
268            report.schema_version, NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION
269        )));
270    }
271    if report.network != MAINNET_NETWORK {
272        return Err(invalid_validation(format!(
273            "network is {}, expected {MAINNET_NETWORK}",
274            report.network
275        )));
276    }
277    if report.governance_canister_id != MAINNET_GOVERNANCE_CANISTER_ID {
278        return Err(invalid_validation(format!(
279            "governance_canister_id is {}, expected {MAINNET_GOVERNANCE_CANISTER_ID}",
280            report.governance_canister_id
281        )));
282    }
283    if report.collection_page_count == 0 {
284        return Err(invalid_validation(
285            "complete distribution report must retain at least one collection page",
286        ));
287    }
288    let minimum_neuron_count = u64::from(report.collection_page_count - 1);
289    if report.collected_neuron_count < minimum_neuron_count {
290        return Err(invalid_validation(format!(
291            "collection_page_count {} requires at least {minimum_neuron_count} collected neurons, found {}",
292            report.collection_page_count, report.collected_neuron_count
293        )));
294    }
295    validate_governance_time_interval(
296        "collection_started_at",
297        &report.collection_started_at,
298        "collection_updated_at",
299        &report.collection_updated_at,
300    )
301    .map_err(|error| invalid_validation(error.to_string()))?;
302    if report.point_in_time_guaranteed {
303        return Err(invalid_validation(
304            "sequential public-neuron collection cannot claim a point-in-time snapshot",
305        ));
306    }
307    validate_governance_report_source(&report.network, &report.source).map_err(|error| {
308        let context = match &report.source {
309            NnsGovernanceSourceProvenance::ReplicaQuery { .. } => "source",
310            NnsGovernanceSourceProvenance::ReplicatedInterCanisterCall { .. } => "provenance",
311        };
312        invalid_validation(format!("invalid collection {context}: {error}"))
313    })?;
314    validate_retrieval_range(report)
315}
316
317fn validate_retrieval_range(
318    report: &NnsNeuronDistributionReport,
319) -> Result<(), NnsNeuronDistributionValidationError> {
320    match (
321        report.earliest_retrieved_at_timestamp_seconds,
322        report.latest_retrieved_at_timestamp_seconds,
323    ) {
324        (None, None) if report.collected_neuron_count == 0 => Ok(()),
325        (Some(earliest), Some(latest))
326            if report.collected_neuron_count > 0 && earliest <= latest =>
327        {
328            Ok(())
329        }
330        _ => Err(invalid_validation(
331            "retrieval timestamp range disagrees with collected_neuron_count or is reversed",
332        )),
333    }
334}
335
336fn validate_distribution_summary(
337    report: &NnsNeuronDistributionReport,
338) -> Result<(), NnsNeuronDistributionValidationError> {
339    validate_optional_summary(
340        report.reported_staked_maturity_neuron_count,
341        report.unreported_staked_maturity_neuron_count,
342        report.total_reported_staked_maturity_e8s_equivalent,
343        report.collected_neuron_count,
344        "staked maturity",
345    )?;
346    validate_optional_summary(
347        report.reported_deciding_voting_power_neuron_count,
348        report.unreported_deciding_voting_power_neuron_count,
349        report.total_reported_deciding_voting_power,
350        report.collected_neuron_count,
351        "deciding voting power",
352    )?;
353    validate_optional_summary(
354        report.reported_potential_voting_power_neuron_count,
355        report.unreported_potential_voting_power_neuron_count,
356        report.total_reported_potential_voting_power,
357        report.collected_neuron_count,
358        "potential voting power",
359    )?;
360    for (field, count) in [
361        (
362            "known_neuron_metadata_count",
363            report.known_neuron_metadata_count,
364        ),
365        (
366            "neurons_fund_join_timestamp_present_count",
367            report.neurons_fund_join_timestamp_present_count,
368        ),
369    ] {
370        if count > report.collected_neuron_count {
371            return Err(invalid_validation(format!(
372                "{field} {count} exceeds collected_neuron_count {}",
373                report.collected_neuron_count
374            )));
375        }
376    }
377    Ok(())
378}
379
380fn validate_optional_summary(
381    reported: u64,
382    unreported: u64,
383    total: u64,
384    collected: u64,
385    field: &'static str,
386) -> Result<(), NnsNeuronDistributionValidationError> {
387    let accounted = reported
388        .checked_add(unreported)
389        .ok_or_else(|| invalid_validation(format!("{field} coverage count overflow")))?;
390    if accounted != collected {
391        return Err(invalid_validation(format!(
392            "{field} coverage accounts for {accounted} neurons, expected {collected}"
393        )));
394    }
395    if reported == 0 && total != 0 {
396        return Err(invalid_validation(format!(
397            "{field} total must be zero when no rows report the field"
398        )));
399    }
400    Ok(())
401}
402
403fn validate_state_distribution(
404    report: &NnsNeuronDistributionReport,
405) -> Result<(), NnsNeuronDistributionValidationError> {
406    let mut previous = None;
407    let mut neuron_count = 0_u64;
408    let mut stake = 0_u64;
409    for row in &report.state_distribution {
410        if previous.is_some_and(|state| state >= row.state) {
411            return Err(invalid_validation(
412                "state distribution is not strictly raw-code ordered",
413            ));
414        }
415        if row.state_text != NnsNeuronState::from_code(row.state) {
416            return Err(invalid_validation(format!(
417                "state classification for raw code {} is inconsistent",
418                row.state
419            )));
420        }
421        neuron_count = add_distribution_count(neuron_count, row.neuron_count, "state")?;
422        stake = add_validation_total(stake, row.effective_stake_e8s, "state stake")?;
423        previous = Some(row.state);
424    }
425    validate_distribution_totals(report, neuron_count, stake, "state")
426}
427
428fn validate_visibility_distribution(
429    report: &NnsNeuronDistributionReport,
430) -> Result<(), NnsNeuronDistributionValidationError> {
431    let mut previous: Option<Option<i32>> = None;
432    let mut neuron_count = 0_u64;
433    let mut stake = 0_u64;
434    for row in &report.visibility_distribution {
435        if previous.is_some_and(|visibility| visibility >= row.visibility) {
436            return Err(invalid_validation(
437                "visibility distribution is not strictly optional-raw-code ordered",
438            ));
439        }
440        if row.visibility_text != NnsNeuronVisibility::from_code(row.visibility) {
441            return Err(invalid_validation(format!(
442                "visibility classification for raw code {:?} is inconsistent",
443                row.visibility
444            )));
445        }
446        neuron_count = add_distribution_count(neuron_count, row.neuron_count, "visibility")?;
447        stake = add_validation_total(stake, row.effective_stake_e8s, "visibility stake")?;
448        previous = Some(row.visibility);
449    }
450    validate_distribution_totals(report, neuron_count, stake, "visibility")
451}
452
453fn validate_neuron_type_distribution(
454    report: &NnsNeuronDistributionReport,
455) -> Result<(), NnsNeuronDistributionValidationError> {
456    let mut previous: Option<Option<i32>> = None;
457    let mut neuron_count = 0_u64;
458    let mut stake = 0_u64;
459    for row in &report.neuron_type_distribution {
460        if previous.is_some_and(|neuron_type| neuron_type >= row.neuron_type) {
461            return Err(invalid_validation(
462                "neuron-type distribution is not strictly optional-raw-code ordered",
463            ));
464        }
465        if row.neuron_type_text != NnsNeuronType::from_code(row.neuron_type) {
466            return Err(invalid_validation(format!(
467                "neuron-type classification for raw code {:?} is inconsistent",
468                row.neuron_type
469            )));
470        }
471        neuron_count = add_distribution_count(neuron_count, row.neuron_count, "neuron-type")?;
472        stake = add_validation_total(stake, row.effective_stake_e8s, "neuron-type stake")?;
473        previous = Some(row.neuron_type);
474    }
475    validate_distribution_totals(report, neuron_count, stake, "neuron-type")
476}
477
478fn add_distribution_count(
479    total: u64,
480    count: u64,
481    dimension: &'static str,
482) -> Result<u64, NnsNeuronDistributionValidationError> {
483    if count == 0 {
484        return Err(invalid_validation(format!(
485            "{dimension} distribution row must contain at least one neuron"
486        )));
487    }
488    add_validation_total(total, count, dimension)
489}
490
491fn add_validation_total(
492    total: u64,
493    value: u64,
494    field: &'static str,
495) -> Result<u64, NnsNeuronDistributionValidationError> {
496    total
497        .checked_add(value)
498        .ok_or_else(|| invalid_validation(format!("{field} total overflow")))
499}
500
501fn validate_distribution_totals(
502    report: &NnsNeuronDistributionReport,
503    neuron_count: u64,
504    stake: u64,
505    dimension: &'static str,
506) -> Result<(), NnsNeuronDistributionValidationError> {
507    if neuron_count != report.collected_neuron_count {
508        return Err(invalid_validation(format!(
509            "{dimension} neuron counts sum to {neuron_count}, expected {}",
510            report.collected_neuron_count
511        )));
512    }
513    if stake != report.total_effective_stake_e8s {
514        return Err(invalid_validation(format!(
515            "{dimension} effective stake sums to {stake}, expected {}",
516            report.total_effective_stake_e8s
517        )));
518    }
519    Ok(())
520}
521
522fn invalid_validation(reason: impl Into<String>) -> NnsNeuronDistributionValidationError {
523    NnsNeuronDistributionValidationError {
524        reason: reason.into(),
525    }
526}
527
528#[derive(Clone, Copy, Default)]
529struct DimensionAccumulator {
530    neuron_count: u64,
531    effective_stake_e8s: u64,
532}
533
534impl DimensionAccumulator {
535    fn observe(
536        &mut self,
537        effective_stake_e8s: u64,
538        count_field: &'static str,
539        stake_field: &'static str,
540    ) -> Result<(), NnsNeuronDistributionError> {
541        increment(&mut self.neuron_count, count_field)?;
542        add(
543            &mut self.effective_stake_e8s,
544            effective_stake_e8s,
545            stake_field,
546        )
547    }
548}
549
550#[derive(Default)]
551struct OptionalValueAccumulator {
552    reported_neuron_count: u64,
553    unreported_neuron_count: u64,
554    total_reported_value: u64,
555}
556
557impl OptionalValueAccumulator {
558    fn observe(
559        &mut self,
560        value: Option<u64>,
561        reported_field: &'static str,
562        unreported_field: &'static str,
563        total_field: &'static str,
564    ) -> Result<(), NnsNeuronDistributionError> {
565        if let Some(value) = value {
566            increment(&mut self.reported_neuron_count, reported_field)?;
567            add(&mut self.total_reported_value, value, total_field)
568        } else {
569            increment(&mut self.unreported_neuron_count, unreported_field)
570        }
571    }
572}
573
574#[derive(Default)]
575struct DistributionAccumulator {
576    states: BTreeMap<i32, DimensionAccumulator>,
577    visibilities: BTreeMap<Option<i32>, DimensionAccumulator>,
578    neuron_types: BTreeMap<Option<i32>, DimensionAccumulator>,
579    total_effective_stake_e8s: u64,
580    staked_maturity: OptionalValueAccumulator,
581    deciding_voting_power: OptionalValueAccumulator,
582    potential_voting_power: OptionalValueAccumulator,
583    known_neuron_metadata_count: u64,
584    neurons_fund_join_timestamp_present_count: u64,
585    earliest_retrieved_at_timestamp_seconds: Option<u64>,
586    latest_retrieved_at_timestamp_seconds: Option<u64>,
587}
588
589impl DistributionAccumulator {
590    fn observe(&mut self, neuron: &NnsNeuronRow) -> Result<(), NnsNeuronDistributionError> {
591        add(
592            &mut self.total_effective_stake_e8s,
593            neuron.stake_e8s,
594            "total_effective_stake_e8s",
595        )?;
596        self.states.entry(neuron.state).or_default().observe(
597            neuron.stake_e8s,
598            "state_neuron_count",
599            "state_effective_stake_e8s",
600        )?;
601        self.visibilities
602            .entry(neuron.visibility)
603            .or_default()
604            .observe(
605                neuron.stake_e8s,
606                "visibility_neuron_count",
607                "visibility_effective_stake_e8s",
608            )?;
609        self.neuron_types
610            .entry(neuron.neuron_type)
611            .or_default()
612            .observe(
613                neuron.stake_e8s,
614                "neuron_type_neuron_count",
615                "neuron_type_effective_stake_e8s",
616            )?;
617        self.staked_maturity.observe(
618            neuron.staked_maturity_e8s_equivalent,
619            "reported_staked_maturity_neuron_count",
620            "unreported_staked_maturity_neuron_count",
621            "total_reported_staked_maturity_e8s_equivalent",
622        )?;
623        self.deciding_voting_power.observe(
624            neuron.deciding_voting_power,
625            "reported_deciding_voting_power_neuron_count",
626            "unreported_deciding_voting_power_neuron_count",
627            "total_reported_deciding_voting_power",
628        )?;
629        self.potential_voting_power.observe(
630            neuron.potential_voting_power,
631            "reported_potential_voting_power_neuron_count",
632            "unreported_potential_voting_power_neuron_count",
633            "total_reported_potential_voting_power",
634        )?;
635        if neuron.known_neuron_data.is_some() {
636            increment(
637                &mut self.known_neuron_metadata_count,
638                "known_neuron_metadata_count",
639            )?;
640        }
641        if neuron.joined_community_fund_timestamp_seconds.is_some() {
642            increment(
643                &mut self.neurons_fund_join_timestamp_present_count,
644                "neurons_fund_join_timestamp_present_count",
645            )?;
646        }
647        let retrieved_at = neuron.retrieved_at_timestamp_seconds;
648        self.earliest_retrieved_at_timestamp_seconds = Some(
649            self.earliest_retrieved_at_timestamp_seconds
650                .map_or(retrieved_at, |earliest| earliest.min(retrieved_at)),
651        );
652        self.latest_retrieved_at_timestamp_seconds = Some(
653            self.latest_retrieved_at_timestamp_seconds
654                .map_or(retrieved_at, |latest| latest.max(retrieved_at)),
655        );
656        Ok(())
657    }
658
659    fn into_report(
660        self,
661        collection: &NnsNeuronCollectionState,
662        collected_neuron_count: u64,
663        source: NnsGovernanceSourceProvenance,
664    ) -> NnsNeuronDistributionReport {
665        NnsNeuronDistributionReport {
666            schema_version: NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION,
667            network: collection.network().to_string(),
668            governance_canister_id: collection.governance_canister_id().to_string(),
669            source,
670            collection_started_at: collection.started_at().to_string(),
671            collection_updated_at: collection.updated_at().to_string(),
672            collection_page_count: collection.pages_fetched(),
673            collected_neuron_count,
674            point_in_time_guaranteed: false,
675            earliest_retrieved_at_timestamp_seconds: self.earliest_retrieved_at_timestamp_seconds,
676            latest_retrieved_at_timestamp_seconds: self.latest_retrieved_at_timestamp_seconds,
677            total_effective_stake_e8s: self.total_effective_stake_e8s,
678            reported_staked_maturity_neuron_count: self.staked_maturity.reported_neuron_count,
679            unreported_staked_maturity_neuron_count: self.staked_maturity.unreported_neuron_count,
680            total_reported_staked_maturity_e8s_equivalent: self
681                .staked_maturity
682                .total_reported_value,
683            reported_deciding_voting_power_neuron_count: self
684                .deciding_voting_power
685                .reported_neuron_count,
686            unreported_deciding_voting_power_neuron_count: self
687                .deciding_voting_power
688                .unreported_neuron_count,
689            total_reported_deciding_voting_power: self.deciding_voting_power.total_reported_value,
690            reported_potential_voting_power_neuron_count: self
691                .potential_voting_power
692                .reported_neuron_count,
693            unreported_potential_voting_power_neuron_count: self
694                .potential_voting_power
695                .unreported_neuron_count,
696            total_reported_potential_voting_power: self.potential_voting_power.total_reported_value,
697            known_neuron_metadata_count: self.known_neuron_metadata_count,
698            neurons_fund_join_timestamp_present_count: self
699                .neurons_fund_join_timestamp_present_count,
700            state_distribution: self
701                .states
702                .into_iter()
703                .map(|(state, distribution)| NnsNeuronStateDistribution {
704                    state,
705                    state_text: NnsNeuronState::from_code(state),
706                    neuron_count: distribution.neuron_count,
707                    effective_stake_e8s: distribution.effective_stake_e8s,
708                })
709                .collect(),
710            visibility_distribution: self
711                .visibilities
712                .into_iter()
713                .map(
714                    |(visibility, distribution)| NnsNeuronVisibilityDistribution {
715                        visibility,
716                        visibility_text: NnsNeuronVisibility::from_code(visibility),
717                        neuron_count: distribution.neuron_count,
718                        effective_stake_e8s: distribution.effective_stake_e8s,
719                    },
720                )
721                .collect(),
722            neuron_type_distribution: self
723                .neuron_types
724                .into_iter()
725                .map(|(neuron_type, distribution)| NnsNeuronTypeDistribution {
726                    neuron_type,
727                    neuron_type_text: NnsNeuronType::from_code(neuron_type),
728                    neuron_count: distribution.neuron_count,
729                    effective_stake_e8s: distribution.effective_stake_e8s,
730                })
731                .collect(),
732        }
733    }
734}
735
736fn increment(value: &mut u64, field: &'static str) -> Result<(), NnsNeuronDistributionError> {
737    *value = value
738        .checked_add(1)
739        .ok_or(NnsNeuronDistributionError::AccountingOverflow { field })?;
740    Ok(())
741}
742
743fn add(total: &mut u64, value: u64, field: &'static str) -> Result<(), NnsNeuronDistributionError> {
744    *total = total
745        .checked_add(value)
746        .ok_or(NnsNeuronDistributionError::AccountingOverflow { field })?;
747    Ok(())
748}
749
750#[cfg(test)]
751mod tests;