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;
17pub 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#[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
55const TROUBLESHOOTING_DOC: &str = "https://0xmiden.github.io/miden-client/cli-troubleshooting.html";
58
59#[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("address {0} available, but its derived note tag {1} is already being tracked")]
70 NoteTagDerivedAddressAlreadyTracked(String, NoteTag),
71 #[error("account error")]
72 AccountError(#[from] AccountError),
73 #[error("account with id {0} is locked")]
74 AccountLocked(AccountId),
75 #[error("network account commitment {0} doesn't match the imported account commitment")]
76 AccountCommitmentMismatch(Word),
77 #[error("account with id {0} is private")]
78 AccountIsPrivate(AccountId),
79 #[error("account nonce is too low to import")]
80 AccountNonceTooLow,
81 #[error("asset error")]
82 AssetError(#[from] AssetError),
83 #[error("account data wasn't found for account id {0}")]
84 AccountDataNotFound(AccountId),
85 #[error("error creating the partial blockchain")]
86 PartialBlockchainError(#[from] PartialBlockchainError),
87 #[error("data deserialization error")]
88 DataDeserializationError(#[from] DeserializationError),
89 #[error("note with id {0} not found on chain")]
90 NoteNotFoundOnChain(NoteId),
91 #[error("error parsing hex")]
92 HexParseError(#[from] HexParseError),
93 #[error("partial MMR has a forest that does not fit within a u32")]
94 InvalidPartialMmrForest,
95 #[error("can't add new account without seed")]
96 AddNewAccountWithoutSeed,
97 #[error("error with merkle path")]
98 MerkleError(#[from] MerkleError),
99 #[error(
100 "the transaction didn't produce the output notes with the expected recipient digests ({0:?})"
101 )]
102 MissingOutputRecipients(Vec<Word>),
103 #[error("note error")]
104 NoteError(#[from] NoteError),
105 #[error("note checker error")]
106 NoteCheckerError(#[from] NoteCheckerError),
107 #[error("note import error: {0}")]
108 NoteImportError(String),
109 #[error("error while converting input note")]
110 NoteRecordConversionError(#[from] NoteRecordError),
111 #[error("transport api error")]
112 NoteTransportError(#[from] NoteTransportError),
113 #[error("no consumable note for account {0}")]
114 NoConsumableNoteForAccount(AccountId),
115 #[error("rpc api error")]
116 RpcError(#[from] RpcError),
117 #[error("recency condition error: {0}")]
118 RecencyConditionError(&'static str),
119 #[error("note screener error")]
120 NoteScreenerError(#[from] NoteScreenerError),
121 #[error("store error")]
122 StoreError(#[from] StoreError),
123 #[error("transaction executor error")]
124 TransactionExecutorError(#[from] TransactionExecutorError),
125 #[error("transaction input error")]
126 TransactionInputError(#[source] TransactionInputError),
127 #[error("transaction prover error")]
128 TransactionProvingError(#[from] TransactionProverError),
129 #[error("transaction request error")]
130 TransactionRequestError(#[from] TransactionRequestError),
131 #[error("transaction script builder error")]
132 AccountInterfaceError(#[from] AccountInterfaceError),
133 #[error("transaction script error")]
134 TransactionScriptError(#[source] TransactionScriptError),
135 #[error("client initialization error: {0}")]
136 ClientInitializationError(String),
137 #[error("note tags limit exceeded (max {0})")]
138 NoteTagsLimitExceeded(usize),
139 #[error("accounts limit exceeded (max {0})")]
140 AccountsLimitExceeded(usize),
141 #[error("unsupported authentication scheme ID: {0}")]
142 UnsupportedAuthSchemeId(u8),
143 #[error("account error is not full: {0}")]
144 AccountRecordNotFull(AccountId),
145 #[error("account error is not partial: {0}")]
146 AccountRecordNotPartial(AccountId),
147}
148
149impl From<ClientError> for String {
153 fn from(err: ClientError) -> String {
154 err.to_string()
155 }
156}
157
158impl From<&ClientError> for Option<ErrorHint> {
159 fn from(err: &ClientError) -> Self {
160 match err {
161 ClientError::MissingOutputRecipients(recipients) => {
162 Some(missing_recipient_hint(recipients))
163 },
164 ClientError::TransactionRequestError(inner) => inner.into(),
165 ClientError::TransactionExecutorError(inner) => transaction_executor_hint(inner),
166 ClientError::NoteNotFoundOnChain(note_id) => Some(ErrorHint {
167 message: format!(
168 "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."
169 ),
170 docs_url: Some(TROUBLESHOOTING_DOC),
171 }),
172 _ => None,
173 }
174 }
175}
176
177impl ClientError {
178 pub fn error_hint(&self) -> Option<ErrorHint> {
179 self.into()
180 }
181}
182
183impl From<&TransactionRequestError> for Option<ErrorHint> {
184 fn from(err: &TransactionRequestError) -> Self {
185 match err {
186 TransactionRequestError::NoInputNotesNorAccountChange => Some(ErrorHint {
187 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(),
188 docs_url: Some(TROUBLESHOOTING_DOC),
189 }),
190 TransactionRequestError::StorageSlotNotFound(slot, account_id) => {
191 Some(storage_miss_hint(*slot, *account_id))
192 },
193 _ => None,
194 }
195 }
196}
197
198impl TransactionRequestError {
199 pub fn error_hint(&self) -> Option<ErrorHint> {
200 self.into()
201 }
202}
203
204fn missing_recipient_hint(recipients: &[Word]) -> ErrorHint {
205 let message = format!(
206 "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."
207 );
208
209 ErrorHint {
210 message,
211 docs_url: Some(TROUBLESHOOTING_DOC),
212 }
213}
214
215fn storage_miss_hint(slot: u8, account_id: AccountId) -> ErrorHint {
216 ErrorHint {
217 message: format!(
218 "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."
219 ),
220 docs_url: Some(TROUBLESHOOTING_DOC),
221 }
222}
223
224fn transaction_executor_hint(err: &TransactionExecutorError) -> Option<ErrorHint> {
225 match err {
226 TransactionExecutorError::ForeignAccountNotAnchoredInReference(account_id) => {
227 Some(ErrorHint {
228 message: format!(
229 "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."
230 ),
231 docs_url: Some(TROUBLESHOOTING_DOC),
232 })
233 },
234 TransactionExecutorError::TransactionProgramExecutionFailed(_) => Some(ErrorHint {
235 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(),
236 docs_url: Some(TROUBLESHOOTING_DOC),
237 }),
238 _ => None,
239 }
240}
241
242#[derive(Debug, Error)]
247pub enum IdPrefixFetchError {
248 #[error("no matches were found with the {0}")]
250 NoMatch(String),
251 #[error("found more than one element for the provided {0} and only one match is expected")]
253 MultipleMatches(String),
254}