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(
70        "address {0} cannot be tracked: its derived note tag {1} is already associated with another tracked address"
71    )]
72    NoteTagDerivedAddressAlreadyTracked(String, NoteTag),
73    #[error("account error")]
74    AccountError(#[from] AccountError),
75    #[error("account {0} is locked because the local state may be out of date with the network")]
76    AccountLocked(AccountId),
77    #[error(
78        "account import failed: the on-chain account commitment ({0}) does not match the commitment of the account being imported"
79    )]
80    AccountCommitmentMismatch(Word),
81    #[error("account {0} is private and its details cannot be retrieved from the network")]
82    AccountIsPrivate(AccountId),
83    #[error(
84        "cannot import account: the local account nonce is higher than the imported one, meaning the local state is newer"
85    )]
86    AccountNonceTooLow,
87    #[error("asset error")]
88    AssetError(#[from] AssetError),
89    #[error("account data wasn't found for account id {0}")]
90    AccountDataNotFound(AccountId),
91    #[error("failed to construct the partial blockchain")]
92    PartialBlockchainError(#[from] PartialBlockchainError),
93    #[error("failed to deserialize data")]
94    DataDeserializationError(#[from] DeserializationError),
95    #[error("note with id {0} not found on chain")]
96    NoteNotFoundOnChain(NoteId),
97    #[error("failed to parse hex string")]
98    HexParseError(#[from] HexParseError),
99    #[error(
100        "the chain Merkle Mountain Range (MMR) forest value exceeds the supported range (must fit in a u32)"
101    )]
102    InvalidPartialMmrForest,
103    #[error(
104        "cannot track a new account without its seed; the seed is required to validate the account ID's correctness"
105    )]
106    AddNewAccountWithoutSeed,
107    #[error("merkle proof error")]
108    MerkleError(#[from] MerkleError),
109    #[error(
110        "transaction output mismatch: expected output notes with recipient digests {0:?} were not produced by the transaction"
111    )]
112    MissingOutputRecipients(Vec<Word>),
113    #[error("note error")]
114    NoteError(#[from] NoteError),
115    #[error("note consumption check failed")]
116    NoteCheckerError(#[from] NoteCheckerError),
117    #[error("note import error: {0}")]
118    NoteImportError(String),
119    #[error("failed to convert note record")]
120    NoteRecordConversionError(#[from] NoteRecordError),
121    #[error("note transport error")]
122    NoteTransportError(#[from] NoteTransportError),
123    #[error(
124        "account {0} has no notes available to consume; sync the client or check that notes targeting this account exist"
125    )]
126    NoConsumableNoteForAccount(AccountId),
127    #[error("RPC error")]
128    RpcError(#[from] RpcError),
129    #[error(
130        "transaction failed a recency check: {0} — the reference block may be too old; try syncing and resubmitting"
131    )]
132    RecencyConditionError(&'static str),
133    #[error("note relevance check failed")]
134    NoteScreenerError(#[from] NoteScreenerError),
135    #[error("storage error")]
136    StoreError(#[from] StoreError),
137    #[error("transaction execution failed")]
138    TransactionExecutorError(#[from] TransactionExecutorError),
139    #[error("invalid transaction input")]
140    TransactionInputError(#[source] TransactionInputError),
141    #[error("transaction proving failed")]
142    TransactionProvingError(#[from] TransactionProverError),
143    #[error("invalid transaction request")]
144    TransactionRequestError(#[from] TransactionRequestError),
145    #[error("failed to build transaction script from account interface")]
146    AccountInterfaceError(#[from] AccountInterfaceError),
147    #[error("transaction script error")]
148    TransactionScriptError(#[source] TransactionScriptError),
149    #[error("client initialization error: {0}")]
150    ClientInitializationError(String),
151    #[error("cannot track more note tags: the maximum of {0} tracked tags has been reached")]
152    NoteTagsLimitExceeded(usize),
153    #[error("cannot track more accounts: the maximum of {0} tracked accounts has been reached")]
154    AccountsLimitExceeded(usize),
155    #[error(
156        "unsupported authentication scheme ID {0}; supported schemes are: RpoFalcon512 (0) and EcdsaK256Keccak (1)"
157    )]
158    UnsupportedAuthSchemeId(u8),
159    #[error("expected full account data for account {0}, but only partial data is available")]
160    AccountRecordNotFull(AccountId),
161    #[error("expected partial account data for account {0}, but full data was found")]
162    AccountRecordNotPartial(AccountId),
163}
164
165// CONVERSIONS
166// ================================================================================================
167
168impl From<ClientError> for String {
169    fn from(err: ClientError) -> String {
170        err.to_string()
171    }
172}
173
174impl From<&ClientError> for Option<ErrorHint> {
175    fn from(err: &ClientError) -> Self {
176        match err {
177            ClientError::MissingOutputRecipients(recipients) => {
178                Some(missing_recipient_hint(recipients))
179            },
180            ClientError::TransactionRequestError(inner) => inner.into(),
181            ClientError::TransactionExecutorError(inner) => transaction_executor_hint(inner),
182            ClientError::NoteNotFoundOnChain(note_id) => Some(ErrorHint {
183                message: format!(
184                    "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."
185                ),
186                docs_url: Some(TROUBLESHOOTING_DOC),
187            }),
188            ClientError::AccountLocked(account_id) => Some(ErrorHint {
189                message: format!(
190                    "Account {account_id} is locked because the client may be missing its latest \
191                     state. This can happen when the account is shared and another client executed \
192                     a transaction. Run `sync` to fetch the latest state from the network."
193                ),
194                docs_url: Some(TROUBLESHOOTING_DOC),
195            }),
196            ClientError::AccountNonceTooLow => Some(ErrorHint {
197                message: "The account you are trying to import has an older nonce than the version \
198                          already tracked locally. Run `sync` to ensure your local state is current, \
199                          or re-export the account from a more up-to-date source.".to_string(),
200                docs_url: Some(TROUBLESHOOTING_DOC),
201            }),
202            ClientError::NoConsumableNoteForAccount(account_id) => Some(ErrorHint {
203                message: format!(
204                    "No notes were found that account {account_id} can consume. \
205                     Run `sync` to fetch the latest notes from the network, \
206                     and verify that notes targeting this account have been committed on chain."
207                ),
208                docs_url: Some(TROUBLESHOOTING_DOC),
209            }),
210            ClientError::RpcError(RpcError::ConnectionError(_)) => Some(ErrorHint {
211                message: "Could not reach the Miden node. Check that the node endpoint in your \
212                          configuration is correct and that the node is running.".to_string(),
213                docs_url: Some(TROUBLESHOOTING_DOC),
214            }),
215            ClientError::RpcError(RpcError::AcceptHeaderError(_)) => Some(ErrorHint {
216                message: "The node rejected the request due to a version mismatch. \
217                          Ensure your client version is compatible with the node version.".to_string(),
218                docs_url: Some(TROUBLESHOOTING_DOC),
219            }),
220            ClientError::AddNewAccountWithoutSeed => Some(ErrorHint {
221                message: "New accounts require a seed to derive their initial state. \
222                          Use `Client::new_account()` which generates the seed automatically, \
223                          or provide the seed when importing.".to_string(),
224                docs_url: Some(TROUBLESHOOTING_DOC),
225            }),
226            _ => None,
227        }
228    }
229}
230
231impl ClientError {
232    pub fn error_hint(&self) -> Option<ErrorHint> {
233        self.into()
234    }
235}
236
237impl From<&TransactionRequestError> for Option<ErrorHint> {
238    fn from(err: &TransactionRequestError) -> Self {
239        match err {
240            TransactionRequestError::NoInputNotesNorAccountChange => Some(ErrorHint {
241                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(),
242                docs_url: Some(TROUBLESHOOTING_DOC),
243            }),
244            TransactionRequestError::StorageSlotNotFound(slot, account_id) => {
245                Some(storage_miss_hint(*slot, *account_id))
246            },
247            TransactionRequestError::InputNoteNotAuthenticated(note_id) => Some(ErrorHint {
248                message: format!(
249                    "Note {note_id} needs an inclusion proof before it can be consumed as an \
250                     authenticated input. Run `sync` to fetch the latest proofs from the network."
251                ),
252                docs_url: Some(TROUBLESHOOTING_DOC),
253            }),
254            TransactionRequestError::P2IDNoteWithoutAsset => Some(ErrorHint {
255                message: "A pay-to-ID (P2ID) note transfers assets to a target account. \
256                          Add at least one fungible or non-fungible asset to the note.".to_string(),
257                docs_url: Some(TROUBLESHOOTING_DOC),
258            }),
259            TransactionRequestError::InvalidSenderAccount(account_id) => Some(ErrorHint {
260                message: format!(
261                    "Account {account_id} is not tracked by this client. Import or create the \
262                     account first, then retry the transaction."
263                ),
264                docs_url: Some(TROUBLESHOOTING_DOC),
265            }),
266            _ => None,
267        }
268    }
269}
270
271impl TransactionRequestError {
272    pub fn error_hint(&self) -> Option<ErrorHint> {
273        self.into()
274    }
275}
276
277fn missing_recipient_hint(recipients: &[Word]) -> ErrorHint {
278    let message = format!(
279        "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."
280    );
281
282    ErrorHint {
283        message,
284        docs_url: Some(TROUBLESHOOTING_DOC),
285    }
286}
287
288fn storage_miss_hint(slot: u8, account_id: AccountId) -> ErrorHint {
289    ErrorHint {
290        message: format!(
291            "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."
292        ),
293        docs_url: Some(TROUBLESHOOTING_DOC),
294    }
295}
296
297fn transaction_executor_hint(err: &TransactionExecutorError) -> Option<ErrorHint> {
298    match err {
299        TransactionExecutorError::ForeignAccountNotAnchoredInReference(account_id) => {
300            Some(ErrorHint {
301                message: format!(
302                    "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."
303                ),
304                docs_url: Some(TROUBLESHOOTING_DOC),
305            })
306        },
307        TransactionExecutorError::TransactionProgramExecutionFailed(_) => Some(ErrorHint {
308            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(),
309            docs_url: Some(TROUBLESHOOTING_DOC),
310        }),
311        _ => None,
312    }
313}
314
315// ID PREFIX FETCH ERROR
316// ================================================================================================
317
318/// Error when Looking for a specific ID from a partial ID.
319#[derive(Debug, Error)]
320pub enum IdPrefixFetchError {
321    /// No matches were found for the ID prefix.
322    #[error("no stored notes matched the provided prefix '{0}'")]
323    NoMatch(String),
324    /// Multiple entities matched with the ID prefix.
325    #[error(
326        "multiple {0} entries match the provided prefix; provide a longer prefix to narrow it down"
327    )]
328    MultipleMatches(String),
329}