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