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 NnsGovernanceSourceProvenance, NnsGovernanceSourceSelection, validate_governance_continuation,
19 validate_governance_request,
20};
21use serde::{Deserialize, Serialize};
22
23pub const NNS_NEURON_COLLECTION_STATE_SCHEMA_VERSION: u32 = 1;
25
26#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
33#[serde(deny_unknown_fields)]
34pub struct NnsNeuronCollectionState {
35 #[serde(flatten)]
36 collection: GovernanceCollectionState,
37 neurons_fetched: u64,
38 next_start_neuron_id: Option<u64>,
39}
40
41impl NnsNeuronCollectionState {
42 pub fn new(
44 request: &NnsGovernanceRequest,
45 page_size: u32,
46 max_pages: u32,
47 ) -> Result<Self, NnsNeuronError> {
48 validate_governance_request(request)?;
49 validate_page_size(page_size)?;
50 if max_pages == 0 {
51 return Err(NnsNeuronError::InvalidCollectionMaxPages);
52 }
53 Ok(Self {
54 collection: GovernanceCollectionState::new(request, page_size, max_pages),
55 neurons_fetched: 0,
56 next_start_neuron_id: None,
57 })
58 }
59
60 #[cfg(feature = "nns-host")]
61 pub(in crate::nns) const fn from_complete_collection(
62 collection: GovernanceCollectionState,
63 rows: u64,
64 ) -> Self {
65 Self {
66 collection,
67 neurons_fetched: rows,
68 next_start_neuron_id: None,
69 }
70 }
71
72 #[cfg(feature = "nns-host")]
73 pub(in crate::nns) fn collection_evidence(
74 &self,
75 ) -> crate::nns::governance::collection::NnsGovernanceCollectionEvidence {
76 crate::nns::governance::collection::NnsGovernanceCollectionEvidence::from_state(
77 &self.collection,
78 )
79 }
80
81 #[must_use]
83 pub const fn schema_version(&self) -> u32 {
84 self.collection.schema_version
85 }
86
87 #[must_use]
89 pub fn network(&self) -> &str {
90 &self.collection.network
91 }
92
93 #[must_use]
95 pub fn governance_canister_id(&self) -> &str {
96 &self.collection.governance_canister_id
97 }
98
99 #[must_use]
101 pub const fn requested_source(&self) -> &NnsGovernanceSourceSelection {
102 &self.collection.requested_source
103 }
104
105 #[must_use]
107 pub const fn source(&self) -> Option<&NnsGovernanceSourceProvenance> {
108 self.collection.source.as_ref()
109 }
110
111 #[must_use]
113 pub const fn page_size(&self) -> u32 {
114 self.collection.page_size
115 }
116
117 #[must_use]
119 pub const fn max_pages(&self) -> u32 {
120 self.collection.max_pages
121 }
122
123 #[must_use]
125 pub const fn pages_fetched(&self) -> u32 {
126 self.collection.pages_fetched
127 }
128
129 #[must_use]
131 pub const fn neurons_fetched(&self) -> u64 {
132 self.neurons_fetched
133 }
134
135 #[must_use]
137 pub const fn next_start_neuron_id(&self) -> Option<u64> {
138 self.next_start_neuron_id
139 }
140
141 #[must_use]
143 pub fn started_at(&self) -> &str {
144 &self.collection.started_at
145 }
146
147 #[must_use]
149 pub fn updated_at(&self) -> &str {
150 &self.collection.updated_at
151 }
152
153 #[must_use]
155 pub const fn status(&self) -> NnsGovernanceCollectionStatus {
156 self.collection.status(self.next_start_neuron_id.is_some())
157 }
158
159 #[must_use]
161 pub const fn is_complete(&self) -> bool {
162 matches!(self.status(), NnsGovernanceCollectionStatus::Complete)
163 }
164}
165
166#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
173pub struct NnsNeuronCollectionStep {
174 pub page: NnsNeuronListReport,
176 pub state: NnsNeuronCollectionState,
178}
179
180#[cfg(feature = "nns-host")]
182pub async fn advance_nns_neuron_collection(
183 request: &NnsGovernanceRequest,
184 state: &NnsNeuronCollectionState,
185) -> Result<NnsNeuronCollectionStep, NnsNeuronHostError> {
186 Ok(
187 advance_nns_neuron_collection_with_source(request, state, &LiveNnsSource::default())
188 .await?,
189 )
190}
191
192pub async fn advance_nns_neuron_collection_with_source(
194 request: &NnsGovernanceRequest,
195 state: &NnsNeuronCollectionState,
196 source: &dyn NnsNeuronSource,
197) -> Result<NnsNeuronCollectionStep, NnsNeuronError> {
198 validate_governance_request(request)?;
199 validate_collection_state(state)?;
200 validate_governance_continuation!(request, state, NnsNeuronError)?;
201
202 let mut page_request = NnsNeuronListRequest::new(request.clone(), state.collection.page_size);
203 page_request.exclusive_start_neuron_id = state.next_start_neuron_id;
204 let page = build_nns_neuron_list_report_with_source(&page_request, source).await?;
205 if let Some(expected) = &state.collection.source
206 && *expected != page.context.source
207 {
208 return Err(NnsNeuronError::CollectionSourceChanged {
209 expected: expected.clone(),
210 actual: page.context.source,
211 });
212 }
213
214 let (pages_fetched, neurons_fetched) = crate::nns::governance::advance_collection_counts(
215 state.collection.pages_fetched,
216 state.neurons_fetched,
217 u64::try_from(page.neurons.len())
218 .map_err(|_| NnsNeuronError::CollectionAccountingOverflow)?,
219 )
220 .ok_or(NnsNeuronError::CollectionAccountingOverflow)?;
221 let next_start_neuron_id = page.next_start_neuron_id;
222 let next_state = NnsNeuronCollectionState {
223 collection: state.collection.after_page(
224 request,
225 page.context.source.clone(),
226 pages_fetched,
227 ),
228 neurons_fetched,
229 next_start_neuron_id,
230 };
231 Ok(NnsNeuronCollectionStep {
232 page,
233 state: next_state,
234 })
235}
236
237pub(super) fn validate_collection_state(
238 state: &NnsNeuronCollectionState,
239) -> Result<(), NnsNeuronError> {
240 let invalid = |reason| NnsNeuronError::InvalidCollectionState { reason };
241 state
242 .collection
243 .validate(NNS_NEURON_COLLECTION_STATE_SCHEMA_VERSION, invalid)?;
244 validate_page_size(state.collection.page_size)?;
245
246 let neurons_fetched = state.neurons_fetched;
247 let maximum_rows = crate::nns::governance::validate_governance_collection_row_bounds(
248 "neurons_fetched",
249 neurons_fetched,
250 state.collection.pages_fetched,
251 state.collection.page_size,
252 invalid,
253 )?;
254
255 let valid_lifecycle = crate::nns::governance::collection_lifecycle_matches(
256 state.status(),
257 state.collection.pages_fetched,
258 state.collection.max_pages,
259 state.neurons_fetched,
260 maximum_rows,
261 state.next_start_neuron_id.is_some(),
262 state.collection.source.is_some(),
263 ) && (state.status() != NnsGovernanceCollectionStatus::Complete
264 || neurons_fetched < maximum_rows);
265 if !valid_lifecycle {
266 return Err(invalid(format!(
267 "status {} disagrees with cursor, provenance, or counters",
268 state.status()
269 )));
270 }
271 Ok(())
272}
273
274#[cfg(test)]
275mod tests;