miden_client/
errors.rs

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