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 let (collection, proposals_fetched) = state.collection.after_page(
137 request,
138 page.context.source.clone(),
139 state.proposals_fetched,
140 page.proposals.len(),
141 |expected, actual| NnsProposalError::CollectionSourceChanged { expected, actual },
142 NnsProposalError::CollectionAccountingOverflow,
143 )?;
144 let page_count = u32::try_from(page.proposals.len())
145 .map_err(|_| NnsProposalError::CollectionAccountingOverflow)?;
146 let next_before_proposal_id = (page_count == state.collection.page_size)
147 .then(|| {
148 page.proposals
149 .iter()
150 .filter_map(|proposal| proposal.proposal_id)
151 .min()
152 .filter(|proposal_id| *proposal_id > 1)
153 })
154 .flatten();
155 let next_state = NnsProposalCollectionState {
156 collection,
157 proposals_fetched,
158 next_before_proposal_id,
159 };
160 Ok(NnsProposalCollectionStep {
161 page,
162 state: next_state,
163 })
164}
165
166pub(super) fn validate_collection_state(
167 state: &NnsProposalCollectionState,
168) -> Result<(), NnsProposalError> {
169 let invalid = |reason| NnsProposalError::InvalidCollectionState { reason };
170 state
171 .collection
172 .validate(NNS_PROPOSAL_COLLECTION_STATE_SCHEMA_VERSION, invalid)?;
173 validate_proposal_page_size(state.collection.page_size)?;
174
175 let proposals_fetched = state.proposals_fetched;
176 let maximum_rows =
177 state
178 .collection
179 .validate_row_bounds("proposals_fetched", proposals_fetched, invalid)?;
180
181 if state
182 .next_before_proposal_id
183 .is_some_and(|cursor| cursor <= 1)
184 {
185 return Err(invalid(
186 "next_before_proposal_id must be greater than one".to_string(),
187 ));
188 }
189
190 state.collection.validate_lifecycle(
191 proposals_fetched,
192 maximum_rows,
193 state.next_before_proposal_id.is_some(),
194 true,
195 invalid,
196 )
197}
198
199#[cfg(test)]
200mod tests;