ic_query/nns/neuron/report/collection/
mod.rs1#[cfg(feature = "nns-host")]
8use super::NnsNeuronHostError;
9use super::{
10 NnsNeuronError,
11 model::{NnsNeuronListReport, NnsNeuronListRequest},
12 source::{NnsNeuronSource, build_nns_neuron_list_report_with_source, validate_page_size},
13};
14#[cfg(feature = "nns-host")]
15use crate::nns::LiveNnsSource;
16use crate::nns::governance::{
17 GovernanceCollectionState, NnsGovernanceCollectionStatus, NnsGovernanceRequest,
18 governance_collection_accessors, validate_governance_continuation, validate_governance_request,
19};
20use serde::{Deserialize, Serialize};
21
22pub const NNS_NEURON_COLLECTION_STATE_SCHEMA_VERSION: u32 = 1;
24
25#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
32#[serde(deny_unknown_fields)]
33pub struct NnsNeuronCollectionState {
34 #[serde(flatten)]
35 collection: GovernanceCollectionState,
36 neurons_fetched: u64,
37 next_start_neuron_id: Option<u64>,
38}
39
40impl NnsNeuronCollectionState {
41 pub fn new(
43 request: &NnsGovernanceRequest,
44 page_size: u32,
45 max_pages: u32,
46 ) -> Result<Self, NnsNeuronError> {
47 validate_governance_request(request)?;
48 validate_page_size(page_size)?;
49 if max_pages == 0 {
50 return Err(NnsNeuronError::InvalidCollectionMaxPages);
51 }
52 Ok(Self {
53 collection: GovernanceCollectionState::new(request, page_size, max_pages),
54 neurons_fetched: 0,
55 next_start_neuron_id: None,
56 })
57 }
58
59 #[cfg(feature = "nns-host")]
60 pub(in crate::nns) const fn from_complete_collection(
61 collection: GovernanceCollectionState,
62 rows: u64,
63 ) -> Self {
64 Self {
65 collection,
66 neurons_fetched: rows,
67 next_start_neuron_id: None,
68 }
69 }
70
71 #[cfg(feature = "nns-host")]
72 pub(in crate::nns) fn collection_evidence(
73 &self,
74 ) -> crate::nns::governance::collection::NnsGovernanceCollectionEvidence {
75 crate::nns::governance::collection::NnsGovernanceCollectionEvidence::from_state(
76 &self.collection,
77 )
78 }
79
80 governance_collection_accessors!(next_start_neuron_id);
81
82 #[must_use]
84 pub const fn neurons_fetched(&self) -> u64 {
85 self.neurons_fetched
86 }
87
88 #[must_use]
90 pub const fn next_start_neuron_id(&self) -> Option<u64> {
91 self.next_start_neuron_id
92 }
93}
94
95#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
102pub struct NnsNeuronCollectionStep {
103 pub page: NnsNeuronListReport,
105 pub state: NnsNeuronCollectionState,
107}
108
109#[cfg(feature = "nns-host")]
111pub async fn advance_nns_neuron_collection(
112 request: &NnsGovernanceRequest,
113 state: &NnsNeuronCollectionState,
114) -> Result<NnsNeuronCollectionStep, NnsNeuronHostError> {
115 Ok(
116 advance_nns_neuron_collection_with_source(request, state, &LiveNnsSource::default())
117 .await?,
118 )
119}
120
121pub async fn advance_nns_neuron_collection_with_source(
123 request: &NnsGovernanceRequest,
124 state: &NnsNeuronCollectionState,
125 source: &dyn NnsNeuronSource,
126) -> Result<NnsNeuronCollectionStep, NnsNeuronError> {
127 validate_governance_request(request)?;
128 validate_collection_state(state)?;
129 validate_governance_continuation!(request, state, NnsNeuronError)?;
130
131 let mut page_request = NnsNeuronListRequest::new(request.clone(), state.collection.page_size);
132 page_request.exclusive_start_neuron_id = state.next_start_neuron_id;
133 let page = build_nns_neuron_list_report_with_source(&page_request, source).await?;
134 if let Some(expected) = &state.collection.source
135 && *expected != page.context.source
136 {
137 return Err(NnsNeuronError::CollectionSourceChanged {
138 expected: expected.clone(),
139 actual: page.context.source,
140 });
141 }
142
143 let (pages_fetched, neurons_fetched) = crate::nns::governance::advance_collection_counts(
144 state.collection.pages_fetched,
145 state.neurons_fetched,
146 u64::try_from(page.neurons.len())
147 .map_err(|_| NnsNeuronError::CollectionAccountingOverflow)?,
148 )
149 .ok_or(NnsNeuronError::CollectionAccountingOverflow)?;
150 let next_start_neuron_id = page.next_start_neuron_id;
151 let next_state = NnsNeuronCollectionState {
152 collection: state.collection.after_page(
153 request,
154 page.context.source.clone(),
155 pages_fetched,
156 ),
157 neurons_fetched,
158 next_start_neuron_id,
159 };
160 Ok(NnsNeuronCollectionStep {
161 page,
162 state: next_state,
163 })
164}
165
166pub(super) fn validate_collection_state(
167 state: &NnsNeuronCollectionState,
168) -> Result<(), NnsNeuronError> {
169 let invalid = |reason| NnsNeuronError::InvalidCollectionState { reason };
170 state
171 .collection
172 .validate(NNS_NEURON_COLLECTION_STATE_SCHEMA_VERSION, invalid)?;
173 validate_page_size(state.collection.page_size)?;
174
175 let neurons_fetched = state.neurons_fetched;
176 let maximum_rows = crate::nns::governance::validate_governance_collection_row_bounds(
177 "neurons_fetched",
178 neurons_fetched,
179 state.collection.pages_fetched,
180 state.collection.page_size,
181 invalid,
182 )?;
183
184 let valid_lifecycle = crate::nns::governance::collection_lifecycle_matches(
185 state.status(),
186 state.collection.pages_fetched,
187 state.collection.max_pages,
188 state.neurons_fetched,
189 maximum_rows,
190 state.next_start_neuron_id.is_some(),
191 state.collection.source.is_some(),
192 ) && (state.status() != NnsGovernanceCollectionStatus::Complete
193 || neurons_fetched < maximum_rows);
194 if !valid_lifecycle {
195 return Err(invalid(format!(
196 "status {} disagrees with cursor, provenance, or counters",
197 state.status()
198 )));
199 }
200 Ok(())
201}
202
203#[cfg(test)]
204mod tests;