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::HexParseError;
22use miden_tx::utils::serde::DeserializationError;
23use miden_tx::{NoteCheckerError, TransactionExecutorError, TransactionProverError};
24use thiserror::Error;
25
26use crate::note::NoteScreenerError;
27use crate::note_transport::NoteTransportError;
28use crate::rpc::RpcError;
29use crate::store::{NoteRecordError, StoreError};
30use crate::transaction::TransactionRequestError;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct ErrorHint {
37 message: String,
38 docs_url: Option<&'static str>,
39}
40
41impl ErrorHint {
42 pub fn into_help_message(self) -> String {
43 self.to_string()
44 }
45}
46
47impl fmt::Display for ErrorHint {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 match self.docs_url {
50 Some(url) => write!(f, "{} See docs: {}", self.message, url),
51 None => f.write_str(self.message.as_str()),
52 }
53 }
54}
55
56const TROUBLESHOOTING_DOC: &str = "https://0xmiden.github.io/miden-client/cli-troubleshooting.html";
59
60#[derive(Debug, Error)]
65pub enum ClientError {
66 #[error("address {0} is already being tracked")]
67 AddressAlreadyTracked(String),
68 #[error("account with id {0} is already being tracked")]
69 AccountAlreadyTracked(AccountId),
70 #[error(
71 "address {0} cannot be tracked: its derived note tag {1} is already associated with another tracked address"
72 )]
73 NoteTagDerivedAddressAlreadyTracked(String, NoteTag),
74 #[error("account error")]
75 AccountError(#[from] AccountError),
76 #[error("account {0} is locked because the local state may be out of date with the network")]
77 AccountLocked(AccountId),
78 #[error(
79 "account import failed: the on-chain account commitment ({0}) does not match the commitment of the account being imported"
80 )]
81 AccountCommitmentMismatch(Word),
82 #[error("account {0} is private and its details cannot be retrieved from the network")]
83 AccountIsPrivate(AccountId),
84 #[error("account with id {0} not found on the network")]
85 AccountNotFoundOnChain(AccountId),
86 #[error(
87 "cannot import account: the local account nonce is higher than the imported one, meaning the local state is newer"
88 )]
89 AccountNonceTooLow,
90 #[error("asset error")]
91 AssetError(#[from] AssetError),
92 #[error("account data wasn't found for account id {0}")]
93 AccountDataNotFound(AccountId),
94 #[error("failed to construct the partial blockchain")]
95 PartialBlockchainError(#[from] PartialBlockchainError),
96 #[error("failed to deserialize data")]
97 DataDeserializationError(#[from] DeserializationError),
98 #[error("note with id {0} not found on chain")]
99 NoteNotFoundOnChain(NoteId),
100 #[error("failed to parse hex string")]
101 HexParseError(#[from] HexParseError),
102 #[error(
103 "the chain Merkle Mountain Range (MMR) forest value exceeds the supported range (must fit in a u32)"
104 )]
105 InvalidPartialMmrForest,
106 #[error(
107 "cannot track a new account without its seed; the seed is required to validate the account ID's correctness"
108 )]
109 AddNewAccountWithoutSeed,
110 #[error("merkle proof error")]
111 MerkleError(#[from] MerkleError),
112 #[error(
113 "transaction output mismatch: expected output notes with recipient digests {0:?} were not produced by the transaction"
114 )]
115 MissingOutputRecipients(Vec<Word>),
116 #[error("note error")]
117 NoteError(#[from] NoteError),
118 #[error("note consumption check failed")]
119 NoteCheckerError(#[from] NoteCheckerError),
120 #[error("note import error: {0}")]
121 NoteImportError(String),
122 #[error("failed to convert note record")]
123 NoteRecordConversionError(#[from] NoteRecordError),
124 #[error("note transport error")]
125 NoteTransportError(#[from] NoteTransportError),
126 #[error(
127 "account {0} has no notes available to consume; sync the client or check that notes targeting this account exist"
128 )]
129 NoConsumableNoteForAccount(AccountId),
130 #[error("RPC error")]
131 RpcError(#[from] RpcError),
132 #[error(
133 "transaction failed a recency check: {0} — the reference block may be too old; try syncing and resubmitting"
134 )]
135 RecencyConditionError(&'static str),
136 #[error("note relevance check failed")]
137 NoteScreenerError(#[from] NoteScreenerError),
138 #[error("storage error")]
139 StoreError(#[from] StoreError),
140 #[error("transaction execution failed")]
141 TransactionExecutorError(#[from] TransactionExecutorError),
142 #[error("invalid transaction input")]
143 TransactionInputError(#[source] TransactionInputError),
144 #[error("transaction proving failed")]
145 TransactionProvingError(#[from] TransactionProverError),
146 #[error("invalid transaction request")]
147 TransactionRequestError(#[from] TransactionRequestError),
148 #[error("failed to build transaction script from account interface")]
149 AccountInterfaceError(#[from] AccountInterfaceError),
150 #[error("transaction script error")]
151 TransactionScriptError(#[source] TransactionScriptError),
152 #[error("client initialization error: {0}")]
153 ClientInitializationError(String),
154 #[error("cannot track more note tags: the maximum of {0} tracked tags has been reached")]
155 NoteTagsLimitExceeded(u32),
156 #[error("cannot track more accounts: the maximum of {0} tracked accounts has been reached")]
157 AccountsLimitExceeded(u32),
158 #[error("expected full account data for account {0}, but only partial data is available")]
159 AccountRecordNotFull(AccountId),
160 #[error("expected partial account data for account {0}, but full data was found")]
161 AccountRecordNotPartial(AccountId),
162 #[error("failed to register NTX note script with root {script_root:?}")]
163 NtxScriptRegistrationFailed {
164 script_root: Word,
165 #[source]
166 source: RpcError,
167 },
168}
169
170impl From<ClientError> for String {
174 fn from(err: ClientError) -> String {
175 err.to_string()
176 }
177}
178
179impl From<&ClientError> for Option<ErrorHint> {
180 fn from(err: &ClientError) -> Self {
181 match err {
182 ClientError::MissingOutputRecipients(recipients) => {
183 Some(missing_recipient_hint(recipients))
184 },
185 ClientError::TransactionRequestError(inner) => inner.into(),
186 ClientError::TransactionExecutorError(inner) => transaction_executor_hint(inner),
187 ClientError::NoteNotFoundOnChain(note_id) => Some(ErrorHint {
188 message: format!(
189 "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."
190 ),
191 docs_url: Some(TROUBLESHOOTING_DOC),
192 }),
193 ClientError::AccountLocked(account_id) => Some(ErrorHint {
194 message: format!(
195 "Account {account_id} is locked because the client may be missing its latest \
196 state. This can happen when the account is shared and another client executed \
197 a transaction. Run `sync` to fetch the latest state from the network."
198 ),
199 docs_url: Some(TROUBLESHOOTING_DOC),
200 }),
201 ClientError::AccountNonceTooLow => Some(ErrorHint {
202 message: "The account you are trying to import has an older nonce than the version \
203 already tracked locally. Run `sync` to ensure your local state is current, \
204 or re-export the account from a more up-to-date source.".to_string(),
205 docs_url: Some(TROUBLESHOOTING_DOC),
206 }),
207 ClientError::NoConsumableNoteForAccount(account_id) => Some(ErrorHint {
208 message: format!(
209 "No notes were found that account {account_id} can consume. \
210 Run `sync` to fetch the latest notes from the network, \
211 and verify that notes targeting this account have been committed on chain."
212 ),
213 docs_url: Some(TROUBLESHOOTING_DOC),
214 }),
215 ClientError::RpcError(RpcError::ConnectionError(_)) => Some(ErrorHint {
216 message: "Could not reach the Miden node. Check that the node endpoint in your \
217 configuration is correct and that the node is running.".to_string(),
218 docs_url: Some(TROUBLESHOOTING_DOC),
219 }),
220 ClientError::RpcError(RpcError::AcceptHeaderError(_)) => Some(ErrorHint {
221 message: "The node rejected the request due to a version mismatch. \
222 Ensure your client version is compatible with the node version.".to_string(),
223 docs_url: Some(TROUBLESHOOTING_DOC),
224 }),
225 ClientError::AddNewAccountWithoutSeed => Some(ErrorHint {
226 message: "New accounts require a seed to derive their initial state. \
227 Use `Client::new_account()` which generates the seed automatically, \
228 or provide the seed when importing.".to_string(),
229 docs_url: Some(TROUBLESHOOTING_DOC),
230 }),
231 _ => None,
232 }
233 }
234}
235
236impl ClientError {
237 pub fn error_hint(&self) -> Option<ErrorHint> {
238 self.into()
239 }
240}
241
242impl From<&TransactionRequestError> for Option<ErrorHint> {
243 fn from(err: &TransactionRequestError) -> Self {
244 match err {
245 TransactionRequestError::NoInputNotesNorAccountChange => Some(ErrorHint {
246 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(),
247 docs_url: Some(TROUBLESHOOTING_DOC),
248 }),
249 TransactionRequestError::StorageSlotNotFound(slot, account_id) => {
250 Some(storage_miss_hint(*slot, *account_id))
251 },
252 TransactionRequestError::InputNoteNotAuthenticated(note_id) => Some(ErrorHint {
253 message: format!(
254 "Note {note_id} needs an inclusion proof before it can be consumed as an \
255 authenticated input. Run `sync` to fetch the latest proofs from the network."
256 ),
257 docs_url: Some(TROUBLESHOOTING_DOC),
258 }),
259 TransactionRequestError::P2IDNoteWithoutAsset => Some(ErrorHint {
260 message: "A pay-to-ID (P2ID) note transfers assets to a target account. \
261 Add at least one fungible or non-fungible asset to the note.".to_string(),
262 docs_url: Some(TROUBLESHOOTING_DOC),
263 }),
264 TransactionRequestError::InvalidSenderAccount(account_id) => Some(ErrorHint {
265 message: format!(
266 "Account {account_id} is not tracked by this client. Import or create the \
267 account first, then retry the transaction."
268 ),
269 docs_url: Some(TROUBLESHOOTING_DOC),
270 }),
271 _ => None,
272 }
273 }
274}
275
276impl TransactionRequestError {
277 pub fn error_hint(&self) -> Option<ErrorHint> {
278 self.into()
279 }
280}
281
282fn missing_recipient_hint(recipients: &[Word]) -> ErrorHint {
283 let message = format!(
284 "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."
285 );
286
287 ErrorHint {
288 message,
289 docs_url: Some(TROUBLESHOOTING_DOC),
290 }
291}
292
293fn storage_miss_hint(slot: u8, account_id: AccountId) -> ErrorHint {
294 ErrorHint {
295 message: format!(
296 "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."
297 ),
298 docs_url: Some(TROUBLESHOOTING_DOC),
299 }
300}
301
302fn transaction_executor_hint(err: &TransactionExecutorError) -> Option<ErrorHint> {
303 match err {
304 TransactionExecutorError::ForeignAccountNotAnchoredInReference(account_id) => {
305 Some(ErrorHint {
306 message: format!(
307 "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."
308 ),
309 docs_url: Some(TROUBLESHOOTING_DOC),
310 })
311 },
312 TransactionExecutorError::TransactionProgramExecutionFailed(_) => Some(ErrorHint {
313 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(),
314 docs_url: Some(TROUBLESHOOTING_DOC),
315 }),
316 _ => None,
317 }
318}
319
320#[derive(Debug, Error)]
325pub enum IdPrefixFetchError {
326 #[error("no stored notes matched the provided prefix '{0}'")]
328 NoMatch(String),
329 #[error(
331 "multiple {0} entries match the provided prefix; provide a longer prefix to narrow it down"
332 )]
333 MultipleMatches(String),
334}