ic_query/nns/proposals/report/collection/
mod.rs1#[cfg(feature = "nns-host")]
8use super::NnsProposalHostError;
9use super::{
10 NnsProposalError,
11 model::{NnsProposalListReport, NnsProposalListRequest},
12 source::{
13 NnsProposalSource, build_nns_proposal_list_report_with_source, validate_proposal_page_size,
14 },
15};
16#[cfg(feature = "nns-host")]
17use crate::nns::LiveNnsSource;
18use crate::nns::governance::{
19 GovernanceCollectionState, NnsGovernanceRequest, governance_collection_accessors,
20 validate_governance_continuation, validate_governance_request,
21};
22use serde::{Deserialize, Serialize};
23
24pub const NNS_PROPOSAL_COLLECTION_STATE_SCHEMA_VERSION: u32 = 1;
26
27#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
34#[serde(deny_unknown_fields)]
35pub struct NnsProposalCollectionState {
36 #[serde(flatten)]
37 collection: GovernanceCollectionState,
38 proposals_fetched: u64,
39 next_before_proposal_id: Option<u64>,
40}
41
42impl NnsProposalCollectionState {
43 pub fn new(
45 request: &NnsGovernanceRequest,
46 page_size: u32,
47 max_pages: u32,
48 ) -> Result<Self, NnsProposalError> {
49 validate_governance_request(request)?;
50 validate_proposal_page_size(page_size)?;
51 if max_pages == 0 {
52 return Err(NnsProposalError::InvalidCollectionMaxPages);
53 }
54 Ok(Self {
55 collection: GovernanceCollectionState::new(request, page_size, max_pages),
56 proposals_fetched: 0,
57 next_before_proposal_id: None,
58 })
59 }
60
61 #[cfg(feature = "nns-host")]
62 pub(in crate::nns) const fn from_complete_collection(
63 collection: GovernanceCollectionState,
64 rows: u64,
65 ) -> Self {
66 Self {
67 collection,
68 proposals_fetched: rows,
69 next_before_proposal_id: None,
70 }
71 }
72
73 #[cfg(feature = "nns-host")]
74 pub(in crate::nns) fn collection_evidence(
75 &self,
76 ) -> crate::nns::governance::collection::NnsGovernanceCollectionEvidence {
77 crate::nns::governance::collection::NnsGovernanceCollectionEvidence::from_state(
78 &self.collection,
79 )
80 }
81
82 governance_collection_accessors!(next_before_proposal_id);
83
84 #[must_use]
86 pub const fn proposals_fetched(&self) -> u64 {
87 self.proposals_fetched
88 }
89
90 #[must_use]
92 pub const fn next_before_proposal_id(&self) -> Option<u64> {
93 self.next_before_proposal_id
94 }
95}
96
97#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
104pub struct NnsProposalCollectionStep {
105 pub page: NnsProposalListReport,
107 pub state: NnsProposalCollectionState,
109}
110
111#[cfg(feature = "nns-host")]
113pub async fn advance_nns_proposal_collection(
114 request: &NnsGovernanceRequest,
115 state: &NnsProposalCollectionState,
116) -> Result<NnsProposalCollectionStep, NnsProposalHostError> {
117 Ok(
118 advance_nns_proposal_collection_with_source(request, state, &LiveNnsSource::default())
119 .await?,
120 )
121}
122
123pub async fn advance_nns_proposal_collection_with_source(
125 request: &NnsGovernanceRequest,
126 state: &NnsProposalCollectionState,
127 source: &dyn NnsProposalSource,
128) -> Result<NnsProposalCollectionStep, NnsProposalError> {
129 validate_governance_request(request)?;
130 validate_collection_state(state)?;
131 validate_governance_continuation!(request, state, NnsProposalError)?;
132
133 let mut page_request = NnsProposalListRequest::new(request.clone(), state.collection.page_size);
134 page_request.before_proposal_id = state.next_before_proposal_id;
135 let page = build_nns_proposal_list_report_with_source(&page_request, source).await?;
136 if let Some(expected) = &state.collection.source
137 && *expected != page.context.source
138 {
139 return Err(NnsProposalError::CollectionSourceChanged {
140 expected: expected.clone(),
141 actual: page.context.source,
142 });
143 }
144
145 let page_count = u32::try_from(page.proposals.len())
146 .map_err(|_| NnsProposalError::CollectionAccountingOverflow)?;
147 let (pages_fetched, proposals_fetched) = crate::nns::governance::advance_collection_counts(
148 state.collection.pages_fetched,
149 state.proposals_fetched,
150 u64::from(page_count),
151 )
152 .ok_or(NnsProposalError::CollectionAccountingOverflow)?;
153 let next_before_proposal_id = (page_count == state.collection.page_size)
154 .then(|| {
155 page.proposals
156 .iter()
157 .filter_map(|proposal| proposal.proposal_id)
158 .min()
159 .filter(|proposal_id| *proposal_id > 1)
160 })
161 .flatten();
162 let next_state = NnsProposalCollectionState {
163 collection: state.collection.after_page(
164 request,
165 page.context.source.clone(),
166 pages_fetched,
167 ),
168 proposals_fetched,
169 next_before_proposal_id,
170 };
171 Ok(NnsProposalCollectionStep {
172 page,
173 state: next_state,
174 })
175}
176
177pub(super) fn validate_collection_state(
178 state: &NnsProposalCollectionState,
179) -> Result<(), NnsProposalError> {
180 let invalid = |reason| NnsProposalError::InvalidCollectionState { reason };
181 state
182 .collection
183 .validate(NNS_PROPOSAL_COLLECTION_STATE_SCHEMA_VERSION, invalid)?;
184 validate_proposal_page_size(state.collection.page_size)?;
185
186 let proposals_fetched = state.proposals_fetched;
187 let maximum_rows = crate::nns::governance::validate_governance_collection_row_bounds(
188 "proposals_fetched",
189 proposals_fetched,
190 state.collection.pages_fetched,
191 state.collection.page_size,
192 invalid,
193 )?;
194
195 if state
196 .next_before_proposal_id
197 .is_some_and(|cursor| cursor <= 1)
198 {
199 return Err(invalid(
200 "next_before_proposal_id must be greater than one".to_string(),
201 ));
202 }
203
204 let valid_lifecycle = crate::nns::governance::collection_lifecycle_matches(
205 state.status(),
206 state.collection.pages_fetched,
207 state.collection.max_pages,
208 state.proposals_fetched,
209 maximum_rows,
210 state.next_before_proposal_id.is_some(),
211 state.collection.source.is_some(),
212 );
213 if !valid_lifecycle {
214 return Err(invalid(format!(
215 "status {} disagrees with cursor, provenance, or counters",
216 state.status()
217 )));
218 }
219 Ok(())
220}
221
222#[cfg(test)]
223mod tests;