ic_query/nns/proposals/report/error.rs
1//! Module: nns::proposals::report::error
2//!
3//! Responsibility: expose portable and native-wrapper NNS proposal failures.
4//! Does not own: transport execution, cache mechanics, or process presentation.
5//! Boundary: keeps canister/custom callers independent of native host dependencies.
6
7use crate::nns::governance::{NnsGovernanceError, NnsGovernanceSourceProvenance};
8#[cfg(feature = "nns-host")]
9use crate::{
10 HostCacheError, nns::governance::NnsGovernanceAttemptReadError, runtime::RuntimeError,
11};
12#[cfg(feature = "nns-host")]
13use std::path::PathBuf;
14use thiserror::Error as ThisError;
15
16///
17/// NnsProposalError
18///
19/// Portable failure returned while collecting or assembling proposal reports.
20///
21
22#[derive(Debug, ThisError)]
23pub enum NnsProposalError {
24 /// Shared Governance request, transport, or provenance validation failed.
25 #[error(transparent)]
26 Governance(#[from] NnsGovernanceError),
27
28 /// The requested page size is outside the bounded proposal contract.
29 #[error("invalid NNS proposal page limit {limit}; expected 1..={maximum}")]
30 InvalidLimit {
31 /// Requested live-page row limit.
32 limit: u32,
33 /// Largest limit supported by Governance.
34 maximum: u32,
35 },
36
37 /// Governance returned more proposal rows than requested.
38 #[error("NNS proposal page returned {actual} rows; requested at most {requested}")]
39 PageTooLarge {
40 /// Number of rows returned by the source.
41 actual: usize,
42 /// Row ceiling supplied to the source.
43 requested: u32,
44 },
45
46 /// A proposal page row did not include its required identifier.
47 #[error("NNS proposal list page returned a row without a proposal id")]
48 MissingProposalIdInPage,
49
50 /// A proposal page used the reserved zero identifier.
51 #[error("NNS proposal list page returned proposal id zero")]
52 InvalidProposalIdInPage,
53
54 /// A proposal page repeated an identifier.
55 #[error("NNS proposal page returned duplicate proposal id {proposal_id}")]
56 DuplicateProposalId {
57 /// Identifier repeated within the returned page.
58 proposal_id: u64,
59 },
60
61 /// A proposal did not satisfy the exclusive page cursor.
62 #[error(
63 "NNS proposal page returned id {proposal_id}; expected every id below {before_proposal_id}"
64 )]
65 ProposalCursorMismatch {
66 /// Returned proposal that violates the exclusive cursor.
67 proposal_id: u64,
68 /// Requested exclusive upper proposal-id bound.
69 before_proposal_id: u64,
70 },
71
72 /// Governance did not return the requested proposal.
73 #[error("NNS proposal {proposal_id} was not found")]
74 ProposalNotFound {
75 /// Exact identifier requested by the caller.
76 proposal_id: u64,
77 },
78
79 /// Governance returned a different proposal than requested.
80 #[error("NNS proposal detail returned id {actual:?}; expected {expected}")]
81 ProposalIdMismatch {
82 /// Exact identifier requested by the caller.
83 expected: u64,
84 /// Identifier supplied in the returned row, if present.
85 actual: Option<u64>,
86 },
87
88 /// A resumable collection was configured without any page capacity.
89 #[error("invalid NNS proposal collection page limit 0; expected at least one page")]
90 InvalidCollectionMaxPages,
91
92 /// Serialized or caller-modified resumable collection state is inconsistent.
93 #[error("invalid NNS proposal collection state: {reason}")]
94 InvalidCollectionState {
95 /// Deterministic state invariant that failed.
96 reason: String,
97 },
98
99 /// A continuation request changed an identity fixed when collection started.
100 #[error(
101 "NNS proposal collection request changed {field}: received {actual}, expected {expected}"
102 )]
103 CollectionRequestMismatch {
104 /// Fixed request field that changed.
105 field: &'static str,
106 /// Value retained by the collection state.
107 expected: String,
108 /// Value supplied by the continuation request.
109 actual: String,
110 },
111
112 /// A caller attempted to advance a collection that already exhausted the API.
113 #[error("NNS proposal collection is already complete after {pages_fetched} pages")]
114 CollectionComplete {
115 /// Pages admitted before API exhaustion.
116 pages_fetched: u32,
117 },
118
119 /// A caller attempted to advance a collection after consuming its page budget.
120 #[error(
121 "NNS proposal collection reached its {max_pages}-page limit after {pages_fetched} pages"
122 )]
123 CollectionPageLimitReached {
124 /// Pages admitted before the stopped advance.
125 pages_fetched: u32,
126 /// Configured cumulative page ceiling.
127 max_pages: u32,
128 },
129
130 /// A later collection page was returned by a different collector.
131 #[error("NNS proposal collection source changed: received {actual:?}, expected {expected:?}")]
132 CollectionSourceChanged {
133 /// Concrete source retained from the first admitted page.
134 expected: NnsGovernanceSourceProvenance,
135 /// Concrete source returned with the candidate page.
136 actual: NnsGovernanceSourceProvenance,
137 },
138
139 /// Cumulative page or row accounting exceeded its integer representation.
140 #[error("NNS proposal collection accounting overflow")]
141 CollectionAccountingOverflow,
142}
143
144///
145/// NnsProposalHostError
146///
147/// Native wrapper failure for proposal live calls, caches, and refreshes.
148///
149
150#[cfg(feature = "nns-host")]
151#[derive(Debug, ThisError)]
152pub enum NnsProposalHostError {
153 /// Portable proposal collection or validation failed.
154 #[error(transparent)]
155 Proposal(#[from] NnsProposalError),
156
157 /// Confined cache IO or refresh locking failed.
158 #[error(transparent)]
159 Cache(#[from] HostCacheError),
160
161 /// A snapshot's stored identity disagrees with the requested collection.
162 #[error(
163 "cached NNS proposal snapshot identity mismatch at {}: {field} is {actual}, expected {expected}",
164 path.display()
165 )]
166 CacheIdentityMismatch {
167 /// Snapshot file with inconsistent identity.
168 path: PathBuf,
169 /// Identity field that failed validation.
170 field: &'static str,
171 /// Value required by the requested collection.
172 expected: String,
173 /// Value found in the snapshot.
174 actual: String,
175 },
176
177 /// Collection stopped before API exhaustion and did not replace the snapshot.
178 #[error(
179 "NNS proposal refresh did not publish a complete snapshot after {pages_fetched} pages and {rows_fetched} rows: {reason}"
180 )]
181 IncompleteRefresh {
182 /// Number of pages admitted before collection stopped.
183 pages_fetched: u32,
184 /// Number of rows admitted before collection stopped.
185 rows_fetched: usize,
186 /// Failure to establish a complete sequential collection.
187 reason: String,
188 },
189
190 /// Refresh requested a page size outside the supported range.
191 #[error("invalid NNS proposal refresh page size {page_size}; expected 1..={max_page_size}")]
192 InvalidRefreshPageSize {
193 /// Rejected per-page row limit.
194 page_size: u32,
195 /// Largest supported per-page row limit.
196 max_page_size: u32,
197 },
198
199 /// A cache-only read could not find the required complete snapshot.
200 #[error("NNS proposals cache is missing at {}\n\nRun `icq nns proposal refresh` to fetch a complete snapshot.", path.display())]
201 MissingProposalCache {
202 /// Expected complete snapshot path.
203 path: PathBuf,
204 },
205
206 /// A retained refresh-attempt sidecar failed its current contract.
207 #[error("invalid NNS proposal refresh attempt at {}: {reason}", path.display())]
208 InvalidRefreshAttempt {
209 /// Refresh-attempt sidecar that failed validation.
210 path: PathBuf,
211 /// Sidecar validation diagnostic.
212 reason: String,
213 },
214
215 /// A retained proposal snapshot failed its current contract.
216 #[error("invalid NNS proposal cache at {}: {reason}", path.display())]
217 InvalidCache {
218 /// Snapshot file that failed validation.
219 path: PathBuf,
220 /// Snapshot validation diagnostic.
221 reason: String,
222 },
223
224 /// The native runtime bridge could not execute the query.
225 #[error("failed to create Tokio runtime for NNS proposal query: {0}")]
226 Runtime(#[from] RuntimeError),
227}
228
229#[cfg(feature = "nns-host")]
230impl From<NnsGovernanceAttemptReadError> for NnsProposalHostError {
231 fn from(error: NnsGovernanceAttemptReadError) -> Self {
232 match error {
233 NnsGovernanceAttemptReadError::Cache(error) => Self::Cache(error),
234 NnsGovernanceAttemptReadError::Invalid { path, reason } => {
235 Self::InvalidRefreshAttempt { path, reason }
236 }
237 }
238 }
239}