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_distribution(
258        report,
259        "state",
260        "raw-code",
261        report.state_distribution.iter().map(|row| {
262            (
263                row.state,
264                row.state_text == NnsNeuronState::from_code(row.state),
265                row.neuron_count,
266                row.effective_stake_e8s,
267            )
268        }),
269    )?;
270    validate_distribution(
271        report,
272        "visibility",
273        "optional-raw-code",
274        report.visibility_distribution.iter().map(|row| {
275            (
276                row.visibility,
277                row.visibility_text == NnsNeuronVisibility::from_code(row.visibility),
278                row.neuron_count,
279                row.effective_stake_e8s,
280            )
281        }),
282    )?;
283    validate_distribution(
284        report,
285        "neuron-type",
286        "optional-raw-code",
287        report.neuron_type_distribution.iter().map(|row| {
288            (
289                row.neuron_type,
290                row.neuron_type_text == NnsNeuronType::from_code(row.neuron_type),
291                row.neuron_count,
292                row.effective_stake_e8s,
293            )
294        }),
295    )
296}
297
298fn validate_distribution_header(
299    report: &NnsNeuronDistributionReport,
300) -> Result<(), NnsNeuronDistributionValidationError> {
301    if report.schema_version != NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION {
302        return Err(invalid_validation(format!(
303            "schema version {} does not equal {}",
304            report.schema_version, NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION
305        )));
306    }
307    if report.network != MAINNET_NETWORK {
308        return Err(invalid_validation(format!(
309            "network is {}, expected {MAINNET_NETWORK}",
310            report.network
311        )));
312    }
313    if report.governance_canister_id != MAINNET_GOVERNANCE_CANISTER_ID {
314        return Err(invalid_validation(format!(
315            "governance_canister_id is {}, expected {MAINNET_GOVERNANCE_CANISTER_ID}",
316            report.governance_canister_id
317        )));
318    }
319    if report.collection_page_count == 0 {
320        return Err(invalid_validation(
321            "complete distribution report must retain at least one collection page",
322        ));
323    }
324    let minimum_neuron_count = u64::from(report.collection_page_count - 1);
325    if report.collected_neuron_count < minimum_neuron_count {
326        return Err(invalid_validation(format!(
327            "collection_page_count {} requires at least {minimum_neuron_count} collected neurons, found {}",
328            report.collection_page_count, report.collected_neuron_count
329        )));
330    }
331    validate_governance_time_interval(
332        "collection_started_at",
333        &report.collection_started_at,
334        "collection_updated_at",
335        &report.collection_updated_at,
336    )
337    .map_err(|error| invalid_validation(error.to_string()))?;
338    if report.point_in_time_guaranteed {
339        return Err(invalid_validation(
340            "sequential public-neuron collection cannot claim a point-in-time snapshot",
341        ));
342    }
343    validate_governance_report_source(&report.network, &report.source).map_err(|error| {
344        let context = match &report.source {
345            NnsGovernanceSourceProvenance::ReplicaQuery { .. } => "source",
346            NnsGovernanceSourceProvenance::ReplicatedInterCanisterCall { .. } => "provenance",
347        };
348        invalid_validation(format!("invalid collection {context}: {error}"))
349    })?;
350    validate_retrieval_range(report)
351}
352
353fn validate_retrieval_range(
354    report: &NnsNeuronDistributionReport,
355) -> Result<(), NnsNeuronDistributionValidationError> {
356    match (
357        report.earliest_retrieved_at_timestamp_seconds,
358        report.latest_retrieved_at_timestamp_seconds,
359    ) {
360        (None, None) if report.collected_neuron_count == 0 => Ok(()),
361        (Some(earliest), Some(latest))
362            if report.collected_neuron_count > 0 && earliest <= latest =>
363        {
364            Ok(())
365        }
366        _ => Err(invalid_validation(
367            "retrieval timestamp range disagrees with collected_neuron_count or is reversed",
368        )),
369    }
370}
371
372fn validate_distribution_summary(
373    report: &NnsNeuronDistributionReport,
374) -> Result<(), NnsNeuronDistributionValidationError> {
375    validate_optional_summary(
376        report.reported_staked_maturity_neuron_count,
377        report.unreported_staked_maturity_neuron_count,
378        report.total_reported_staked_maturity_e8s_equivalent,
379        report.collected_neuron_count,
380        "staked maturity",
381    )?;
382    validate_optional_summary(
383        report.reported_deciding_voting_power_neuron_count,
384        report.unreported_deciding_voting_power_neuron_count,
385        report.total_reported_deciding_voting_power,
386        report.collected_neuron_count,
387        "deciding voting power",
388    )?;
389    validate_optional_summary(
390        report.reported_potential_voting_power_neuron_count,
391        report.unreported_potential_voting_power_neuron_count,
392        report.total_reported_potential_voting_power,
393        report.collected_neuron_count,
394        "potential voting power",
395    )?;
396    for (field, count) in [
397        (
398            "known_neuron_metadata_count",
399            report.known_neuron_metadata_count,
400        ),
401        (
402            "neurons_fund_join_timestamp_present_count",
403            report.neurons_fund_join_timestamp_present_count,
404        ),
405    ] {
406        if count > report.collected_neuron_count {
407            return Err(invalid_validation(format!(
408                "{field} {count} exceeds collected_neuron_count {}",
409                report.collected_neuron_count
410            )));
411        }
412    }
413    Ok(())
414}
415
416fn validate_optional_summary(
417    reported: u64,
418    unreported: u64,
419    total: u64,
420    collected: u64,
421    field: &'static str,
422) -> Result<(), NnsNeuronDistributionValidationError> {
423    let accounted = reported
424        .checked_add(unreported)
425        .ok_or_else(|| invalid_validation(format!("{field} coverage count overflow")))?;
426    if accounted != collected {
427        return Err(invalid_validation(format!(
428            "{field} coverage accounts for {accounted} neurons, expected {collected}"
429        )));
430    }
431    if reported == 0 && total != 0 {
432        return Err(invalid_validation(format!(
433            "{field} total must be zero when no rows report the field"
434        )));
435    }
436    Ok(())
437}
438
439fn validate_distribution<Key: Copy + Ord + std::fmt::Debug>(
440    report: &NnsNeuronDistributionReport,
441    dimension: &'static str,
442    order: &'static str,
443    rows: impl Iterator<Item = (Key, bool, u64, u64)>,
444) -> Result<(), NnsNeuronDistributionValidationError> {
445    let mut previous = None;
446    let mut neuron_count = 0_u64;
447    let mut stake = 0_u64;
448    for (key, classification_matches, count, effective_stake) in rows {
449        if previous.is_some_and(|previous| previous >= key) {
450            return Err(invalid_validation(format!(
451                "{dimension} distribution is not strictly {order} ordered"
452            )));
453        }
454        if !classification_matches {
455            return Err(invalid_validation(format!(
456                "{dimension} classification for raw code {key:?} is inconsistent"
457            )));
458        }
459        neuron_count = add_distribution_count(neuron_count, count, dimension)?;
460        stake = stake
461            .checked_add(effective_stake)
462            .ok_or_else(|| invalid_validation(format!("{dimension} stake total overflow")))?;
463        previous = Some(key);
464    }
465    validate_distribution_totals(report, neuron_count, stake, dimension)
466}
467
468fn add_distribution_count(
469    total: u64,
470    count: u64,
471    dimension: &'static str,
472) -> Result<u64, NnsNeuronDistributionValidationError> {
473    if count == 0 {
474        return Err(invalid_validation(format!(
475            "{dimension} distribution row must contain at least one neuron"
476        )));
477    }
478    add_validation_total(total, count, dimension)
479}
480
481fn add_validation_total(
482    total: u64,
483    value: u64,
484    field: &'static str,
485) -> Result<u64, NnsNeuronDistributionValidationError> {
486    total
487        .checked_add(value)
488        .ok_or_else(|| invalid_validation(format!("{field} total overflow")))
489}
490
491fn validate_distribution_totals(
492    report: &NnsNeuronDistributionReport,
493    neuron_count: u64,
494    stake: u64,
495    dimension: &'static str,
496) -> Result<(), NnsNeuronDistributionValidationError> {
497    if neuron_count != report.collected_neuron_count {
498        return Err(invalid_validation(format!(
499            "{dimension} neuron counts sum to {neuron_count}, expected {}",
500            report.collected_neuron_count
501        )));
502    }
503    if stake != report.total_effective_stake_e8s {
504        return Err(invalid_validation(format!(
505            "{dimension} effective stake sums to {stake}, expected {}",
506            report.total_effective_stake_e8s
507        )));
508    }
509    Ok(())
510}
511
512fn invalid_validation(reason: impl Into<String>) -> NnsNeuronDistributionValidationError {
513    NnsNeuronDistributionValidationError {
514        reason: reason.into(),
515    }
516}
517
518#[derive(Clone, Copy, Default)]
519struct DimensionAccumulator {
520    neuron_count: u64,
521    effective_stake_e8s: u64,
522}
523
524impl DimensionAccumulator {
525    fn observe(
526        &mut self,
527        effective_stake_e8s: u64,
528        count_field: &'static str,
529        stake_field: &'static str,
530    ) -> Result<(), NnsNeuronDistributionError> {
531        increment(&mut self.neuron_count, count_field)?;
532        add(
533            &mut self.effective_stake_e8s,
534            effective_stake_e8s,
535            stake_field,
536        )
537    }
538}
539
540#[derive(Default)]
541struct OptionalValueAccumulator {
542    reported_neuron_count: u64,
543    unreported_neuron_count: u64,
544    total_reported_value: u64,
545}
546
547impl OptionalValueAccumulator {
548    fn observe(
549        &mut self,
550        value: Option<u64>,
551        reported_field: &'static str,
552        unreported_field: &'static str,
553        total_field: &'static str,
554    ) -> Result<(), NnsNeuronDistributionError> {
555        if let Some(value) = value {
556            increment(&mut self.reported_neuron_count, reported_field)?;
557            add(&mut self.total_reported_value, value, total_field)
558        } else {
559            increment(&mut self.unreported_neuron_count, unreported_field)
560        }
561    }
562}
563
564#[derive(Default)]
565struct DistributionAccumulator {
566    states: BTreeMap<i32, DimensionAccumulator>,
567    visibilities: BTreeMap<Option<i32>, DimensionAccumulator>,
568    neuron_types: BTreeMap<Option<i32>, DimensionAccumulator>,
569    total_effective_stake_e8s: u64,
570    staked_maturity: OptionalValueAccumulator,
571    deciding_voting_power: OptionalValueAccumulator,
572    potential_voting_power: OptionalValueAccumulator,
573    known_neuron_metadata_count: u64,
574    neurons_fund_join_timestamp_present_count: u64,
575    earliest_retrieved_at_timestamp_seconds: Option<u64>,
576    latest_retrieved_at_timestamp_seconds: Option<u64>,
577}
578
579impl DistributionAccumulator {
580    fn observe(&mut self, neuron: &NnsNeuronRow) -> Result<(), NnsNeuronDistributionError> {
581        add(
582            &mut self.total_effective_stake_e8s,
583            neuron.stake_e8s,
584            "total_effective_stake_e8s",
585        )?;
586        self.states.entry(neuron.state).or_default().observe(
587            neuron.stake_e8s,
588            "state_neuron_count",
589            "state_effective_stake_e8s",
590        )?;
591        self.visibilities
592            .entry(neuron.visibility)
593            .or_default()
594            .observe(
595                neuron.stake_e8s,
596                "visibility_neuron_count",
597                "visibility_effective_stake_e8s",
598            )?;
599        self.neuron_types
600            .entry(neuron.neuron_type)
601            .or_default()
602            .observe(
603                neuron.stake_e8s,
604                "neuron_type_neuron_count",
605                "neuron_type_effective_stake_e8s",
606            )?;
607        self.staked_maturity.observe(
608            neuron.staked_maturity_e8s_equivalent,
609            "reported_staked_maturity_neuron_count",
610            "unreported_staked_maturity_neuron_count",
611            "total_reported_staked_maturity_e8s_equivalent",
612        )?;
613        self.deciding_voting_power.observe(
614            neuron.deciding_voting_power,
615            "reported_deciding_voting_power_neuron_count",
616            "unreported_deciding_voting_power_neuron_count",
617            "total_reported_deciding_voting_power",
618        )?;
619        self.potential_voting_power.observe(
620            neuron.potential_voting_power,
621            "reported_potential_voting_power_neuron_count",
622            "unreported_potential_voting_power_neuron_count",
623            "total_reported_potential_voting_power",
624        )?;
625        if neuron.known_neuron_data.is_some() {
626            increment(
627                &mut self.known_neuron_metadata_count,
628                "known_neuron_metadata_count",
629            )?;
630        }
631        if neuron.joined_community_fund_timestamp_seconds.is_some() {
632            increment(
633                &mut self.neurons_fund_join_timestamp_present_count,
634                "neurons_fund_join_timestamp_present_count",
635            )?;
636        }
637        let retrieved_at = neuron.retrieved_at_timestamp_seconds;
638        self.earliest_retrieved_at_timestamp_seconds = Some(
639            self.earliest_retrieved_at_timestamp_seconds
640                .map_or(retrieved_at, |earliest| earliest.min(retrieved_at)),
641        );
642        self.latest_retrieved_at_timestamp_seconds = Some(
643            self.latest_retrieved_at_timestamp_seconds
644                .map_or(retrieved_at, |latest| latest.max(retrieved_at)),
645        );
646        Ok(())
647    }
648
649    fn into_report(
650        self,
651        collection: &NnsNeuronCollectionState,
652        collected_neuron_count: u64,
653        source: NnsGovernanceSourceProvenance,
654    ) -> NnsNeuronDistributionReport {
655        NnsNeuronDistributionReport {
656            schema_version: NNS_NEURON_DISTRIBUTION_REPORT_SCHEMA_VERSION,
657            network: collection.network().to_string(),
658            governance_canister_id: collection.governance_canister_id().to_string(),
659            source,
660            collection_started_at: collection.started_at().to_string(),
661            collection_updated_at: collection.updated_at().to_string(),
662            collection_page_count: collection.pages_fetched(),
663            collected_neuron_count,
664            point_in_time_guaranteed: false,
665            earliest_retrieved_at_timestamp_seconds: self.earliest_retrieved_at_timestamp_seconds,
666            latest_retrieved_at_timestamp_seconds: self.latest_retrieved_at_timestamp_seconds,
667            total_effective_stake_e8s: self.total_effective_stake_e8s,
668            reported_staked_maturity_neuron_count: self.staked_maturity.reported_neuron_count,
669            unreported_staked_maturity_neuron_count: self.staked_maturity.unreported_neuron_count,
670            total_reported_staked_maturity_e8s_equivalent: self
671                .staked_maturity
672                .total_reported_value,
673            reported_deciding_voting_power_neuron_count: self
674                .deciding_voting_power
675                .reported_neuron_count,
676            unreported_deciding_voting_power_neuron_count: self
677                .deciding_voting_power
678                .unreported_neuron_count,
679            total_reported_deciding_voting_power: self.deciding_voting_power.total_reported_value,
680            reported_potential_voting_power_neuron_count: self
681                .potential_voting_power
682                .reported_neuron_count,
683            unreported_potential_voting_power_neuron_count: self
684                .potential_voting_power
685                .unreported_neuron_count,
686            total_reported_potential_voting_power: self.potential_voting_power.total_reported_value,
687            known_neuron_metadata_count: self.known_neuron_metadata_count,
688            neurons_fund_join_timestamp_present_count: self
689                .neurons_fund_join_timestamp_present_count,
690            state_distribution: self
691                .states
692                .into_iter()
693                .map(|(state, distribution)| NnsNeuronStateDistribution {
694                    state,
695                    state_text: NnsNeuronState::from_code(state),
696                    neuron_count: distribution.neuron_count,
697                    effective_stake_e8s: distribution.effective_stake_e8s,
698                })
699                .collect(),
700            visibility_distribution: self
701                .visibilities
702                .into_iter()
703                .map(
704                    |(visibility, distribution)| NnsNeuronVisibilityDistribution {
705                        visibility,
706                        visibility_text: NnsNeuronVisibility::from_code(visibility),
707                        neuron_count: distribution.neuron_count,
708                        effective_stake_e8s: distribution.effective_stake_e8s,
709                    },
710                )
711                .collect(),
712            neuron_type_distribution: self
713                .neuron_types
714                .into_iter()
715                .map(|(neuron_type, distribution)| NnsNeuronTypeDistribution {
716                    neuron_type,
717                    neuron_type_text: NnsNeuronType::from_code(neuron_type),
718                    neuron_count: distribution.neuron_count,
719                    effective_stake_e8s: distribution.effective_stake_e8s,
720                })
721                .collect(),
722        }
723    }
724}
725
726fn increment(value: &mut u64, field: &'static str) -> Result<(), NnsNeuronDistributionError> {
727    *value = value
728        .checked_add(1)
729        .ok_or(NnsNeuronDistributionError::AccountingOverflow { field })?;
730    Ok(())
731}
732
733fn add(total: &mut u64, value: u64, field: &'static str) -> Result<(), NnsNeuronDistributionError> {
734    *total = total
735        .checked_add(value)
736        .ok_or(NnsNeuronDistributionError::AccountingOverflow { field })?;
737    Ok(())
738}
739
740#[cfg(test)]
741mod tests;