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