Skip to main content

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}