1use alloc::boxed::Box;
2use alloc::string::{String, ToString};
3use alloc::vec::Vec;
4use core::fmt;
5
6use miden_protocol::Word;
7use miden_protocol::account::AccountId;
8use miden_protocol::crypto::merkle::MerkleError;
9pub use miden_protocol::errors::{
10 AccountError,
11 AccountIdError,
12 AccountPatchError,
13 AssetError,
14 NetworkIdError,
15};
16use miden_protocol::errors::{
17 NoteError,
18 PartialBlockchainError,
19 ProposedBatchError,
20 ProvenBatchError,
21 TransactionInputError,
22 TransactionScriptError,
23};
24use miden_protocol::note::NoteId;
25use miden_protocol::transaction::TransactionId;
26pub use miden_standards::errors::CodeBuilderError;
29use miden_standards::tx_script::SendNotesTransactionScriptError;
30pub use miden_tx::AuthenticationError;
31use miden_tx::utils::HexParseError;
32use miden_tx::utils::serde::DeserializationError;
33use miden_tx::{
34 DataStoreError,
35 NoteCheckerError,
36 TransactionExecutorError,
37 TransactionProverError,
38};
39use thiserror::Error;
40
41use crate::note::NoteScreenerError;
42use crate::note_transport::NoteTransportError;
43use crate::rpc::RpcError;
44use crate::store::{NoteRecordError, StoreError};
45use crate::transaction::{
46 BatchBuilderError,
47 ChainAnchorError,
48 TransactionRequestError,
49 TransactionStoreUpdateError,
50};
51
52#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct ErrorHint {
57 message: String,
58 docs_url: Option<&'static str>,
59}
60
61impl ErrorHint {
62 pub fn into_help_message(self) -> String {
63 self.to_string()
64 }
65}
66
67impl fmt::Display for ErrorHint {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self.docs_url {
70 Some(url) => write!(f, "{} See docs: {}", self.message, url),
71 None => f.write_str(self.message.as_str()),
72 }
73 }
74}
75
76const TROUBLESHOOTING_DOC: &str =
79 "https://docs.miden.xyz/builder/tools/clients/rust-client/cli/cli-troubleshooting";
80
81#[derive(Debug, Error)]
86pub enum ClientError {
87 #[error("address {0} is already being tracked")]
88 AddressAlreadyTracked(String),
89 #[error("account with id {0} is already being tracked")]
90 AccountAlreadyTracked(AccountId),
91 #[error("account error")]
92 AccountError(#[from] AccountError),
93 #[error("account patch error")]
94 AccountPatchError(#[from] AccountPatchError),
95 #[error("account {0} is locked because the local state may be out of date with the network")]
96 AccountLocked(AccountId),
97 #[error(
98 "account import failed: the on-chain account commitment ({0}) does not match the commitment of the account being imported"
99 )]
100 AccountCommitmentMismatch(Word),
101 #[error("account {0} is private and its details cannot be retrieved from the network")]
102 AccountIsPrivate(AccountId),
103 #[error("account {0} is watched and cannot be used to execute transactions")]
104 AccountIsWatched(AccountId),
105 #[error(
106 "account {0} is already tracked with a different ClientAccountType; switching between Native and Watched is not supported"
107 )]
108 AccountWatchedMismatch(AccountId),
109 #[error("account with id {0} not found on the network")]
110 AccountNotFoundOnChain(AccountId),
111 #[error(
112 "cannot import account: the local account nonce is higher than the imported one, meaning the local state is newer"
113 )]
114 AccountNonceTooLow,
115 #[error("asset error")]
116 AssetError(#[from] AssetError),
117 #[error("account data wasn't found for account id {0}")]
118 AccountDataNotFound(AccountId),
119 #[error(transparent)]
120 BatchBuilder(#[from] BatchBuilderError),
121 #[error("chain anchor error")]
122 ChainAnchorError(#[from] ChainAnchorError),
123 #[error("data store error")]
124 DataStoreError(#[from] DataStoreError),
125 #[error("failed to construct the partial blockchain")]
126 PartialBlockchainError(#[from] PartialBlockchainError),
127 #[error("failed to build proposed batch")]
128 ProposedBatchError(#[from] ProposedBatchError),
129 #[error("failed to prove batch")]
130 ProvenBatchError(#[from] ProvenBatchError),
131 #[error("failed to deserialize data")]
132 DataDeserializationError(#[from] DeserializationError),
133 #[error(
134 "cannot recover consumed note {0}: its nullifier has no position in the sync's transaction execution order"
135 )]
136 MissingConsumedNoteOrder(NoteId),
137 #[error(
138 "cannot continue iterating consumed notes: the store returned the note with details commitment {0}, which carries no consumption position"
139 )]
140 MissingNoteConsumptionPosition(Word),
141 #[error("note with id {0} not found on chain")]
142 NoteNotFoundOnChain(NoteId),
143 #[error("failed to parse hex string")]
144 HexParseError(#[from] HexParseError),
145 #[error(
146 "the chain Merkle Mountain Range (MMR) forest value exceeds the supported range (must fit in a u32)"
147 )]
148 InvalidPartialMmrForest,
149 #[error("chain validation error: {0}")]
150 ChainValidationError(String),
151 #[error(
152 "cannot track a new account without its seed; the seed is required to validate the account ID's correctness"
153 )]
154 AddNewAccountWithoutSeed,
155 #[error("merkle proof error")]
156 MerkleError(#[from] MerkleError),
157 #[error(
158 "transaction output mismatch: expected output notes with recipient digests {0:?} were not produced by the transaction"
159 )]
160 MissingOutputRecipients(Vec<Word>),
161 #[error("note error")]
162 NoteError(#[from] NoteError),
163 #[error("note consumption check failed")]
164 NoteCheckerError(#[from] NoteCheckerError),
165 #[error("note import error: {0}")]
166 NoteImportError(String),
167 #[error("failed to convert note record")]
168 NoteRecordConversionError(#[from] NoteRecordError),
169 #[error("note transport error")]
170 NoteTransportError(#[from] NoteTransportError),
171 #[error(
172 "account {0} has no notes available to consume; sync the client or check that notes targeting this account exist"
173 )]
174 NoConsumableNoteForAccount(AccountId),
175 #[error("RPC error")]
176 RpcError(#[from] RpcError),
177 #[error(
178 "no transaction encryption key is available; the validator set's key must be cached in the store before transaction inputs can be sealed for submission"
179 )]
180 MissingTransactionEncryptionKey,
181 #[error(
182 "transaction failed a recency check: {0} — the reference block may be too old; try syncing and resubmitting"
183 )]
184 RecencyConditionError(&'static str),
185 #[error("note relevance check failed")]
186 NoteScreenerError(#[from] NoteScreenerError),
187 #[error("storage error")]
188 StoreError(#[from] StoreError),
189 #[error("transaction execution failed")]
190 TransactionExecutorError(#[from] TransactionExecutorError),
191 #[error("invalid transaction input")]
192 TransactionInputError(#[source] TransactionInputError),
193 #[error("transaction proving failed")]
194 TransactionProvingError(#[from] TransactionProverError),
195 #[error("prover returned a proof of transaction {returned}, but {requested} was requested")]
196 MismatchedProvenTransaction {
197 requested: TransactionId,
198 returned: TransactionId,
199 },
200 #[error("invalid transaction request")]
201 TransactionRequestError(#[from] TransactionRequestError),
202 #[error("failed to build the send-notes transaction script")]
203 SendNotesTransactionScriptError(#[from] SendNotesTransactionScriptError),
204 #[error("transaction script error")]
205 TransactionScriptError(#[source] TransactionScriptError),
206 #[error("client initialization error: {0}")]
207 ClientInitializationError(String),
208 #[error("expected full account data for account {0}, but only partial data is available")]
209 AccountRecordNotFull(AccountId),
210 #[error("expected partial account data for account {0}, but full data was found")]
211 AccountRecordNotPartial(AccountId),
212 #[error("failed to register NTX note script with root {script_root:?}")]
213 NtxScriptRegistrationFailed {
214 script_root: Word,
215 #[source]
216 source: RpcError,
217 },
218 #[error(
219 "transaction {} was accepted into the node's mempool at block {} but the local store \
220 update failed. The pending store update is attached and can be re-applied later via \
221 `apply_transaction_update`. Resubmitting the same transaction will be rejected if the \
222 original is still in the mempool or has been finalized in a block, because the \
223 account (and network) state has already been mutated by the accepted copy.",
224 pending_update.executed_transaction().id(),
225 pending_update.submission_height()
226 )]
227 ApplyTransactionAfterSubmitFailed {
228 pending_update: Box<crate::transaction::TransactionStoreUpdate>,
229 #[source]
230 source: Box<ClientError>,
231 },
232 #[error(transparent)]
237 Observer(Box<dyn core::error::Error + Send + Sync + 'static>),
238}
239
240pub(crate) fn log_observer_failure(
247 observer: &'static str,
248 op: &str,
249 result: Result<(), ClientError>,
250) {
251 if let Err(err) = result {
252 tracing::warn!(observer, error = ?err, "{} failed; continuing with remaining observers", op);
253 }
254}
255
256impl From<ClientError> for String {
260 fn from(err: ClientError) -> String {
261 err.to_string()
262 }
263}
264
265impl From<TransactionStoreUpdateError> for ClientError {
266 fn from(err: TransactionStoreUpdateError) -> Self {
267 match err {
268 TransactionStoreUpdateError::Store(e) => ClientError::StoreError(e),
269 TransactionStoreUpdateError::NoteScreener(e) => ClientError::NoteScreenerError(e),
270 TransactionStoreUpdateError::NoteRecord(e) => ClientError::NoteRecordConversionError(e),
271 }
272 }
273}
274
275impl From<&ClientError> for Option<ErrorHint> {
276 fn from(err: &ClientError) -> Self {
277 match err {
278 ClientError::MissingOutputRecipients(recipients) => {
279 Some(missing_recipient_hint(recipients))
280 },
281 ClientError::TransactionRequestError(inner) => inner.into(),
282 ClientError::TransactionExecutorError(inner) => transaction_executor_hint(inner),
283 ClientError::NoteNotFoundOnChain(note_id) => Some(ErrorHint {
284 message: format!(
285 "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."
286 ),
287 docs_url: Some(TROUBLESHOOTING_DOC),
288 }),
289 ClientError::AccountLocked(account_id) => Some(ErrorHint {
290 message: format!(
291 "Account {account_id} is locked because the client may be missing its latest \
292 state. This can happen when the account is shared and another client executed \
293 a transaction. Run `sync` to fetch the latest state from the network."
294 ),
295 docs_url: Some(TROUBLESHOOTING_DOC),
296 }),
297 ClientError::AccountNonceTooLow => Some(ErrorHint {
298 message: "The account you are trying to import has an older nonce than the version \
299 already tracked locally. Run `sync` to ensure your local state is current, \
300 or re-export the account from a more up-to-date source.".to_string(),
301 docs_url: Some(TROUBLESHOOTING_DOC),
302 }),
303 ClientError::NoConsumableNoteForAccount(account_id) => Some(ErrorHint {
304 message: format!(
305 "No notes were found that account {account_id} can consume. \
306 Run `sync` to fetch the latest notes from the network, \
307 and verify that notes targeting this account have been committed on chain."
308 ),
309 docs_url: Some(TROUBLESHOOTING_DOC),
310 }),
311 ClientError::RpcError(RpcError::ConnectionError(_)) => Some(ErrorHint {
312 message: "Could not reach the Miden node. Check that the node endpoint in your \
313 configuration is correct and that the node is running.".to_string(),
314 docs_url: Some(TROUBLESHOOTING_DOC),
315 }),
316 ClientError::RpcError(RpcError::AcceptHeaderError(_)) => Some(ErrorHint {
317 message: "The node rejected the request due to a version mismatch. \
318 Ensure your client version is compatible with the node version.".to_string(),
319 docs_url: Some(TROUBLESHOOTING_DOC),
320 }),
321 ClientError::AddNewAccountWithoutSeed => Some(ErrorHint {
322 message: "New accounts require a seed to derive their initial state. \
323 Use `Client::new_account()` which generates the seed automatically, \
324 or provide the seed when importing.".to_string(),
325 docs_url: Some(TROUBLESHOOTING_DOC),
326 }),
327 ClientError::ApplyTransactionAfterSubmitFailed { pending_update, .. } => {
328 let tx_id = pending_update.executed_transaction().id();
329 let submission_height = pending_update.submission_height();
330 Some(ErrorHint {
331 message: format!(
332 "Transaction {tx_id} was accepted into the node's mempool at block \
333 {submission_height} but the local store update failed. The pending \
334 update is attached to this error as `pending_update`; you can re-apply \
335 it later via `Client::apply_transaction_update`. Do NOT resubmit the \
336 same transaction: if the original is still in the mempool or has been \
337 finalized in a block, the account (and network) state has already been \
338 mutated by the accepted copy, so the node will reject the retry."
339 ),
340 docs_url: Some(TROUBLESHOOTING_DOC),
341 })
342 },
343 _ => None,
344 }
345 }
346}
347
348impl ClientError {
349 pub fn error_hint(&self) -> Option<ErrorHint> {
350 self.into()
351 }
352}
353
354impl From<&TransactionRequestError> for Option<ErrorHint> {
355 fn from(err: &TransactionRequestError) -> Self {
356 match err {
357 TransactionRequestError::NoInputNotesNorAccountChange => Some(ErrorHint {
358 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(),
359 docs_url: Some(TROUBLESHOOTING_DOC),
360 }),
361 TransactionRequestError::StorageSlotNotFound(slot, account_id) => {
362 Some(storage_miss_hint(*slot, *account_id))
363 },
364 TransactionRequestError::InputNoteNotAuthenticated(note_id) => Some(ErrorHint {
365 message: format!(
366 "Note {note_id} needs an inclusion proof before it can be consumed as an \
367 authenticated input. Run `sync` to fetch the latest proofs from the network."
368 ),
369 docs_url: Some(TROUBLESHOOTING_DOC),
370 }),
371 TransactionRequestError::P2IDNoteWithoutAsset => Some(ErrorHint {
372 message: "A pay-to-ID (P2ID) note transfers assets to a target account. \
373 Add at least one fungible or non-fungible asset to the note.".to_string(),
374 docs_url: Some(TROUBLESHOOTING_DOC),
375 }),
376 TransactionRequestError::OutputNoteSenderMismatch { expected, actual } => {
377 Some(ErrorHint {
378 message: format!(
379 "A note's sender is the account that emits it: it must be the account \
380 executing the transaction. This transaction runs as account {expected}, \
381 but one of its output notes declares sender {actual}. Rebuild the note \
382 with {expected} as its sender, or execute the transaction from {actual}."
383 ),
384 docs_url: Some(TROUBLESHOOTING_DOC),
385 })
386 },
387 _ => None,
388 }
389 }
390}
391
392impl TransactionRequestError {
393 pub fn error_hint(&self) -> Option<ErrorHint> {
394 self.into()
395 }
396}
397
398fn missing_recipient_hint(recipients: &[Word]) -> ErrorHint {
399 let message = format!(
400 "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."
401 );
402
403 ErrorHint {
404 message,
405 docs_url: Some(TROUBLESHOOTING_DOC),
406 }
407}
408
409fn storage_miss_hint(slot: u8, account_id: AccountId) -> ErrorHint {
410 ErrorHint {
411 message: format!(
412 "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."
413 ),
414 docs_url: Some(TROUBLESHOOTING_DOC),
415 }
416}
417
418fn transaction_executor_hint(err: &TransactionExecutorError) -> Option<ErrorHint> {
419 match err {
420 TransactionExecutorError::ForeignAccountNotAnchoredInReference(account_id) => {
421 Some(ErrorHint {
422 message: format!(
423 "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."
424 ),
425 docs_url: Some(TROUBLESHOOTING_DOC),
426 })
427 },
428 TransactionExecutorError::TransactionProgramExecutionFailed(_) => Some(ErrorHint {
429 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(),
430 docs_url: Some(TROUBLESHOOTING_DOC),
431 }),
432 _ => None,
433 }
434}
435
436#[derive(Debug, Error)]
441pub enum IdPrefixFetchError {
442 #[error("no stored notes matched the provided prefix '{0}'")]
444 NoMatch(String),
445 #[error(
447 "multiple {0} entries match the provided prefix; provide a longer prefix to narrow it down"
448 )]
449 MultipleMatches(String),
450}