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};
17pub 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#[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
54const TROUBLESHOOTING_DOC: &str = "https://0xmiden.github.io/miden-client/cli-troubleshooting.html";
57
58#[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}
137
138impl From<ClientError> for String {
142 fn from(err: ClientError) -> String {
143 err.to_string()
144 }
145}
146
147impl From<&ClientError> for Option<ErrorHint> {
148 fn from(err: &ClientError) -> Self {
149 match err {
150 ClientError::MissingOutputRecipients(recipients) => {
151 Some(missing_recipient_hint(recipients))
152 },
153 ClientError::TransactionRequestError(inner) => inner.into(),
154 ClientError::TransactionExecutorError(inner) => transaction_executor_hint(inner),
155 ClientError::NoteNotFoundOnChain(note_id) => Some(ErrorHint {
156 message: format!(
157 "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."
158 ),
159 docs_url: Some(TROUBLESHOOTING_DOC),
160 }),
161 ClientError::StoreError(StoreError::AccountCommitmentAlreadyExists(commitment)) => {
162 Some(ErrorHint {
163 message: format!(
164 "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."
165 ),
166 docs_url: Some(TROUBLESHOOTING_DOC),
167 })
168 },
169 _ => None,
170 }
171 }
172}
173
174impl ClientError {
175 pub fn error_hint(&self) -> Option<ErrorHint> {
176 self.into()
177 }
178}
179
180impl From<&TransactionRequestError> for Option<ErrorHint> {
181 fn from(err: &TransactionRequestError) -> Self {
182 match err {
183 TransactionRequestError::MissingAuthenticatedInputNote(note_id) => {
184 Some(ErrorHint {
185 message: format!(
186 "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."
187 ),
188 docs_url: Some(TROUBLESHOOTING_DOC),
189 })
190 },
191 TransactionRequestError::NoInputNotesNorAccountChange => Some(ErrorHint {
192 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(),
193 docs_url: Some(TROUBLESHOOTING_DOC),
194 }),
195 TransactionRequestError::StorageSlotNotFound(slot, account_id) => {
196 Some(storage_miss_hint(*slot, *account_id))
197 },
198 _ => None,
199 }
200 }
201}
202
203impl TransactionRequestError {
204 pub fn error_hint(&self) -> Option<ErrorHint> {
205 self.into()
206 }
207}
208
209fn missing_recipient_hint(recipients: &[Word]) -> ErrorHint {
210 let message = format!(
211 "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."
212 );
213
214 ErrorHint {
215 message,
216 docs_url: Some(TROUBLESHOOTING_DOC),
217 }
218}
219
220fn storage_miss_hint(slot: u8, account_id: AccountId) -> ErrorHint {
221 ErrorHint {
222 message: format!(
223 "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."
224 ),
225 docs_url: Some(TROUBLESHOOTING_DOC),
226 }
227}
228
229fn transaction_executor_hint(err: &TransactionExecutorError) -> Option<ErrorHint> {
230 match err {
231 TransactionExecutorError::ForeignAccountNotAnchoredInReference(account_id) => {
232 Some(ErrorHint {
233 message: format!(
234 "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."
235 ),
236 docs_url: Some(TROUBLESHOOTING_DOC),
237 })
238 },
239 TransactionExecutorError::TransactionProgramExecutionFailed(_) => Some(ErrorHint {
240 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(),
241 docs_url: Some(TROUBLESHOOTING_DOC),
242 }),
243 _ => None,
244 }
245}
246
247#[derive(Debug, Error)]
252pub enum IdPrefixFetchError {
253 #[error("no matches were found with the {0}")]
255 NoMatch(String),
256 #[error("found more than one element for the provided {0} and only one match is expected")]
258 MultipleMatches(String),
259}