Skip to main content

miden_client/
errors.rs

1use alloc::string::{String, ToString};
2use alloc::vec::Vec;
3use core::fmt;
4
5use miden_protocol::Word;
6use miden_protocol::account::AccountId;
7use miden_protocol::crypto::merkle::MerkleError;
8pub use miden_protocol::errors::{AccountError, AccountIdError, AssetError, NetworkIdError};
9use miden_protocol::errors::{
10    NoteError,
11    PartialBlockchainError,
12    TransactionInputError,
13    TransactionScriptError,
14};
15use miden_protocol::note::{NoteId, NoteTag};
16use miden_standards::account::interface::AccountInterfaceError;
17// RE-EXPORTS
18// ================================================================================================
19pub use miden_standards::errors::CodeBuilderError;
20pub use miden_tx::AuthenticationError;
21use miden_tx::utils::{DeserializationError, HexParseError};
22use miden_tx::{NoteCheckerError, TransactionExecutorError, TransactionProverError};
23use thiserror::Error;
24
25use crate::note::NoteScreenerError;
26use crate::note_transport::NoteTransportError;
27use crate::rpc::RpcError;
28use crate::store::{NoteRecordError, StoreError};
29use crate::transaction::TransactionRequestError;
30
31// ACTIONABLE HINTS
32// ================================================================================================
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ErrorHint {
36    message: String,
37    docs_url: Option<&'static str>,
38}
39
40impl ErrorHint {
41    pub fn into_help_message(self) -> String {
42        self.to_string()
43    }
44}
45
46impl fmt::Display for ErrorHint {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self.docs_url {
49            Some(url) => write!(f, "{} See docs: {}", self.message, url),
50            None => f.write_str(self.message.as_str()),
51        }
52    }
53}
54
55// TODO: This is mostly illustrative but we could add a URL with fragemtn identifiers
56// for each error
57const TROUBLESHOOTING_DOC: &str = "https://0xmiden.github.io/miden-client/cli-troubleshooting.html";
58
59// CLIENT ERROR
60// ================================================================================================
61
62/// Errors generated by the client.
63#[derive(Debug, Error)]
64pub enum ClientError {
65    #[error("address {0} is already being tracked")]
66    AddressAlreadyTracked(String),
67    #[error("account with id {0} is already being tracked")]
68    AccountAlreadyTracked(AccountId),
69    #[error("address {0} available, but its derived note tag {1} is already being tracked")]
70    NoteTagDerivedAddressAlreadyTracked(String, NoteTag),
71    #[error("account error")]
72    AccountError(#[from] AccountError),
73    #[error("account with id {0} is locked")]
74    AccountLocked(AccountId),
75    #[error("network account commitment {0} doesn't match the imported account commitment")]
76    AccountCommitmentMismatch(Word),
77    #[error("account with id {0} is private")]
78    AccountIsPrivate(AccountId),
79    #[error("account nonce is too low to import")]
80    AccountNonceTooLow,
81    #[error("asset error")]
82    AssetError(#[from] AssetError),
83    #[error("account data wasn't found for account id {0}")]
84    AccountDataNotFound(AccountId),
85    #[error("error creating the partial blockchain")]
86    PartialBlockchainError(#[from] PartialBlockchainError),
87    #[error("data deserialization error")]
88    DataDeserializationError(#[from] DeserializationError),
89    #[error("note with id {0} not found on chain")]
90    NoteNotFoundOnChain(NoteId),
91    #[error("error parsing hex")]
92    HexParseError(#[from] HexParseError),
93    #[error("partial MMR has a forest that does not fit within a u32")]
94    InvalidPartialMmrForest,
95    #[error("can't add new account without seed")]
96    AddNewAccountWithoutSeed,
97    #[error("error with merkle path")]
98    MerkleError(#[from] MerkleError),
99    #[error(
100        "the transaction didn't produce the output notes with the expected recipient digests ({0:?})"
101    )]
102    MissingOutputRecipients(Vec<Word>),
103    #[error("note error")]
104    NoteError(#[from] NoteError),
105    #[error("note checker error")]
106    NoteCheckerError(#[from] NoteCheckerError),
107    #[error("note import error: {0}")]
108    NoteImportError(String),
109    #[error("error while converting input note")]
110    NoteRecordConversionError(#[from] NoteRecordError),
111    #[error("transport api error")]
112    NoteTransportError(#[from] NoteTransportError),
113    #[error("no consumable note for account {0}")]
114    NoConsumableNoteForAccount(AccountId),
115    #[error("rpc api error")]
116    RpcError(#[from] RpcError),
117    #[error("recency condition error: {0}")]
118    RecencyConditionError(&'static str),
119    #[error("note screener error")]
120    NoteScreenerError(#[from] NoteScreenerError),
121    #[error("store error")]
122    StoreError(#[from] StoreError),
123    #[error("transaction executor error")]
124    TransactionExecutorError(#[from] TransactionExecutorError),
125    #[error("transaction input error")]
126    TransactionInputError(#[source] TransactionInputError),
127    #[error("transaction prover error")]
128    TransactionProvingError(#[from] TransactionProverError),
129    #[error("transaction request error")]
130    TransactionRequestError(#[from] TransactionRequestError),
131    #[error("transaction script builder error")]
132    AccountInterfaceError(#[from] AccountInterfaceError),
133    #[error("transaction script error")]
134    TransactionScriptError(#[source] TransactionScriptError),
135    #[error("client initialization error: {0}")]
136    ClientInitializationError(String),
137    #[error("note tags limit exceeded (max {0})")]
138    NoteTagsLimitExceeded(usize),
139    #[error("accounts limit exceeded (max {0})")]
140    AccountsLimitExceeded(usize),
141    #[error("unsupported authentication scheme ID: {0}")]
142    UnsupportedAuthSchemeId(u8),
143    #[error("account error is not full: {0}")]
144    AccountRecordNotFull(AccountId),
145    #[error("account error is not partial: {0}")]
146    AccountRecordNotPartial(AccountId),
147}
148
149// CONVERSIONS
150// ================================================================================================
151
152impl From<ClientError> for String {
153    fn from(err: ClientError) -> String {
154        err.to_string()
155    }
156}
157
158impl From<&ClientError> for Option<ErrorHint> {
159    fn from(err: &ClientError) -> Self {
160        match err {
161            ClientError::MissingOutputRecipients(recipients) => {
162                Some(missing_recipient_hint(recipients))
163            },
164            ClientError::TransactionRequestError(inner) => inner.into(),
165            ClientError::TransactionExecutorError(inner) => transaction_executor_hint(inner),
166            ClientError::NoteNotFoundOnChain(note_id) => Some(ErrorHint {
167                message: format!(
168                    "Note {note_id} has not been found on chain. Double-check the note ID, ensure it has been committed, and run `miden-client sync` before retrying."
169                ),
170                docs_url: Some(TROUBLESHOOTING_DOC),
171            }),
172            _ => None,
173        }
174    }
175}
176
177impl ClientError {
178    pub fn error_hint(&self) -> Option<ErrorHint> {
179        self.into()
180    }
181}
182
183impl From<&TransactionRequestError> for Option<ErrorHint> {
184    fn from(err: &TransactionRequestError) -> Self {
185        match err {
186            TransactionRequestError::NoInputNotesNorAccountChange => Some(ErrorHint {
187                message: "Transactions must consume input notes or mutate tracked account state. Add at least one authenticated/unauthenticated input note or include an explicit account state update in the request.".to_string(),
188                docs_url: Some(TROUBLESHOOTING_DOC),
189            }),
190            TransactionRequestError::StorageSlotNotFound(slot, account_id) => {
191                Some(storage_miss_hint(*slot, *account_id))
192            },
193            _ => None,
194        }
195    }
196}
197
198impl TransactionRequestError {
199    pub fn error_hint(&self) -> Option<ErrorHint> {
200        self.into()
201    }
202}
203
204fn missing_recipient_hint(recipients: &[Word]) -> ErrorHint {
205    let message = format!(
206        "Recipients {recipients:?} were missing from the transaction outputs. Keep `TransactionRequestBuilder::expected_output_recipients(...)` aligned with the MASM program so the declared recipients appear in the outputs."
207    );
208
209    ErrorHint {
210        message,
211        docs_url: Some(TROUBLESHOOTING_DOC),
212    }
213}
214
215fn storage_miss_hint(slot: u8, account_id: AccountId) -> ErrorHint {
216    ErrorHint {
217        message: format!(
218            "Storage slot {slot} was not found on account {account_id}. Verify the account ABI and component ordering, then adjust the slot index used in the transaction."
219        ),
220        docs_url: Some(TROUBLESHOOTING_DOC),
221    }
222}
223
224fn transaction_executor_hint(err: &TransactionExecutorError) -> Option<ErrorHint> {
225    match err {
226        TransactionExecutorError::ForeignAccountNotAnchoredInReference(account_id) => {
227            Some(ErrorHint {
228                message: format!(
229                    "The foreign account proof for {account_id} was built against a different block. Re-fetch the account proof anchored at the request's reference block before retrying."
230                ),
231                docs_url: Some(TROUBLESHOOTING_DOC),
232            })
233        },
234        TransactionExecutorError::TransactionProgramExecutionFailed(_) => Some(ErrorHint {
235            message: "Re-run the transaction with debug mode enabled , capture VM diagnostics, and inspect the source manager output to understand why execution failed.".to_string(),
236            docs_url: Some(TROUBLESHOOTING_DOC),
237        }),
238        _ => None,
239    }
240}
241
242// ID PREFIX FETCH ERROR
243// ================================================================================================
244
245/// Error when Looking for a specific ID from a partial ID.
246#[derive(Debug, Error)]
247pub enum IdPrefixFetchError {
248    /// No matches were found for the ID prefix.
249    #[error("no matches were found with the {0}")]
250    NoMatch(String),
251    /// Multiple entities matched with the ID prefix.
252    #[error("found more than one element for the provided {0} and only one match is expected")]
253    MultipleMatches(String),
254}