Skip to main content

ic_query/icrc/model/
error.rs

1//! Module: icrc::model::error
2//!
3//! Responsibility: typed errors for generic ICRC parsing, reports, and live calls.
4//! Does not own: command dispatch, host calls, or output policy.
5//! Boundary: preserves one public error surface for reusable ICRC query behavior.
6
7#[cfg(feature = "icrc-host")]
8use crate::{HostCacheError, runtime::RuntimeError};
9use std::path::PathBuf;
10use thiserror::Error as ThisError;
11
12///
13/// IcrcError
14///
15/// Error surfaced by generic ICRC validation, report building, and live calls.
16///
17
18#[derive(Debug, ThisError)]
19pub enum IcrcError {
20    /// The native source runtime could not be constructed.
21    #[cfg(feature = "icrc-host")]
22    #[error("failed to create Tokio runtime for ICRC query: {0}")]
23    Runtime(#[from] RuntimeError),
24
25    /// The selected endpoint could not construct a replica agent.
26    #[error("failed to build IC agent for endpoint {endpoint}: {reason}")]
27    AgentBuild {
28        /// Rejected replica endpoint.
29        endpoint: String,
30        /// Agent construction failure.
31        reason: String,
32    },
33
34    /// A supplied principal field could not be parsed.
35    #[error("invalid {field}: {reason}")]
36    InvalidPrincipal {
37        /// Request field containing the principal.
38        field: &'static str,
39        /// Principal parsing failure.
40        reason: String,
41    },
42
43    /// The supplied subaccount is not valid hexadecimal.
44    #[error("invalid subaccount hex: {reason}")]
45    InvalidSubaccountHex {
46        /// Hexadecimal decoding failure.
47        reason: String,
48    },
49
50    /// The decoded subaccount does not contain exactly 32 bytes.
51    #[error("invalid subaccount length: expected 32 bytes, got {bytes}")]
52    InvalidSubaccountLength {
53        /// Observed decoded subaccount length.
54        bytes: usize,
55    },
56
57    /// The typed Candid request could not be encoded.
58    #[error("failed to encode Candid request for {message}: {reason}")]
59    CandidEncode {
60        /// Wire operation whose encoding or decoding failed.
61        message: &'static str,
62        /// Transport, encoding, or decoding failure detail.
63        reason: String,
64    },
65
66    /// The read-only replica query failed.
67    #[error("ICRC ledger method {method} failed: {reason}")]
68    AgentCall {
69        /// Replica method or archive callback context.
70        method: &'static str,
71        /// Transport, encoding, or decoding failure detail.
72        reason: String,
73    },
74
75    /// The response violates its typed Candid contract or decoder budget.
76    #[error("failed to decode Candid response {message}: {reason}")]
77    CandidDecode {
78        /// Wire operation whose encoding or decoding failed.
79        message: &'static str,
80        /// Transport, encoding, or decoding failure detail.
81        reason: String,
82    },
83}
84
85///
86/// IcrcAccountTransactionError
87///
88/// Error surfaced while resolving and querying an ICRC account index.
89///
90
91#[derive(Debug, ThisError)]
92pub enum IcrcAccountTransactionError {
93    /// A cache identity omitted its endpoint.
94    #[error("invalid ICRC account transaction source endpoint {value:?}: {reason}")]
95    InvalidSourceEndpoint {
96        /// Rejected endpoint.
97        value: String,
98        /// Validation failure.
99        reason: String,
100    },
101
102    /// A page or refresh requested an unsupported page size.
103    #[error(
104        "invalid ICRC account transaction page size {page_size}; expected between 1 and {max_page_size}"
105    )]
106    InvalidPageSize {
107        /// Rejected page size.
108        page_size: u32,
109        /// Largest supported page size.
110        max_page_size: u32,
111    },
112
113    /// A custom page source returned data that violates the public page contract.
114    #[error("invalid ICRC account transaction page: {reason}")]
115    InvalidPage {
116        /// Source-result invariant that failed.
117        reason: String,
118    },
119
120    /// A cache view requested no rows.
121    #[error("invalid ICRC account transaction list limit {limit}; expected at least 1")]
122    InvalidListLimit {
123        /// Rejected view limit.
124        limit: u32,
125    },
126
127    /// A diagnostic refresh bound cannot prove any collection progress.
128    #[error("invalid ICRC account transaction max pages {max_pages}; expected at least 1")]
129    InvalidMaxPages {
130        /// Rejected page bound.
131        max_pages: u32,
132    },
133
134    /// A caller supplied a non-decimal or otherwise invalid candid Nat cursor.
135    #[error("invalid ICRC account transaction cursor {value:?}: {reason}")]
136    InvalidCursor {
137        /// Rejected cursor text.
138        value: String,
139        /// Validation failure.
140        reason: String,
141    },
142
143    /// A ledger, index, principal, Candid, or transport operation failed.
144    #[error(transparent)]
145    Query(#[from] IcrcError),
146
147    /// ICRC-106 discovery could not be queried or decoded.
148    #[error(
149        "failed to discover an index for ledger {ledger_canister_id}; supply an explicit index canister: {source}"
150    )]
151    IndexDiscovery {
152        /// Ledger queried for index discovery.
153        ledger_canister_id: String,
154        /// Underlying transport or Candid failure.
155        #[source]
156        source: IcrcError,
157    },
158
159    /// ICRC-106 discovery did not yield an index canister.
160    #[error("ledger {ledger_canister_id} has no usable ICRC index: {reason}")]
161    IndexUnavailable {
162        /// Ledger queried for index discovery.
163        ledger_canister_id: String,
164        /// Discovery result explaining why no index is usable.
165        reason: String,
166    },
167
168    /// The selected index reports a different ledger identity.
169    #[error(
170        "ICRC index {index_canister_id} reports ledger {actual_ledger_canister_id}, expected {expected_ledger_canister_id}"
171    )]
172    IndexLedgerMismatch {
173        /// Index whose `ledger_id` response was checked.
174        index_canister_id: String,
175        /// Ledger requested by the caller.
176        expected_ledger_canister_id: String,
177        /// Ledger reported by the index.
178        actual_ledger_canister_id: String,
179    },
180
181    /// The index returned an application-level account-history error.
182    #[error("ICRC index {index_canister_id} account transaction query failed: {message}")]
183    IndexQuery {
184        /// Index that returned the error.
185        index_canister_id: String,
186        /// Index-provided error message.
187        message: String,
188    },
189
190    /// Complete collection stopped before the source API was exhausted.
191    #[error(
192        "incomplete ICRC account transaction collection after {pages_fetched} page(s) and {rows_fetched} row(s): {reason}"
193    )]
194    IncompleteCollection {
195        /// Verified index used for collection when resolution completed.
196        index_canister_id: Option<String>,
197        /// Successfully fetched pages.
198        pages_fetched: u32,
199        /// Rows retained.
200        rows_fetched: usize,
201        /// Last exclusive cursor when present.
202        last_cursor: Option<String>,
203        /// Reason the collection could not be proven complete.
204        reason: String,
205    },
206
207    /// A page fetch failed after collection had begun.
208    #[error(
209        "ICRC account transaction collection failed after {pages_fetched} page(s) and {rows_fetched} row(s): {source}"
210    )]
211    CollectionPage {
212        /// Verified index used for collection when resolution completed.
213        index_canister_id: Option<String>,
214        /// Successfully fetched pages before the failure.
215        pages_fetched: u32,
216        /// Rows retained before the failure.
217        rows_fetched: usize,
218        /// Last exclusive cursor when present.
219        last_cursor: Option<String>,
220        /// Underlying typed page failure.
221        #[source]
222        source: Box<Self>,
223    },
224
225    /// A custom account-transaction source returned evidence for a different explicit index.
226    #[error(
227        "ICRC account transaction source returned index {actual_index_canister_id}, expected explicitly requested index {expected_index_canister_id}"
228    )]
229    CollectionIndexMismatch {
230        /// Index explicitly requested by the caller.
231        expected_index_canister_id: String,
232        /// Index claimed by the completed collection.
233        actual_index_canister_id: String,
234    },
235
236    /// A complete cache failed semantic validation.
237    #[error("invalid ICRC account transaction cache at {}: {reason}", path.display())]
238    InvalidCache {
239        /// Cache path.
240        path: PathBuf,
241        /// Validation failure.
242        reason: String,
243    },
244
245    /// A refresh-attempt sidecar failed semantic validation.
246    #[error(
247        "invalid ICRC account transaction refresh attempt at {}: {reason}",
248        path.display()
249    )]
250    InvalidRefreshAttempt {
251        /// Attempt sidecar path.
252        path: PathBuf,
253        /// Validation failure.
254        reason: String,
255    },
256
257    /// A cache load, lock, or atomic-write operation failed.
258    #[cfg(feature = "icrc-host")]
259    #[error(transparent)]
260    Cache(#[from] HostCacheError),
261}