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(
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
167impl 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#[derive(Debug, Error)]
322pub enum IdPrefixFetchError {
323 #[error("no stored notes matched the provided prefix '{0}'")]
325 NoMatch(String),
326 #[error(
328 "multiple {0} entries match the provided prefix; provide a longer prefix to narrow it down"
329 )]
330 MultipleMatches(String),
331}