use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use miden_protocol::Word;
use miden_protocol::account::AccountId;
use miden_protocol::crypto::merkle::MerkleError;
pub use miden_protocol::errors::{AccountError, AccountIdError, AssetError, NetworkIdError};
use miden_protocol::errors::{
NoteError,
PartialBlockchainError,
ProposedBatchError,
ProvenBatchError,
TransactionInputError,
TransactionScriptError,
};
use miden_protocol::note::NoteId;
use miden_standards::account::interface::AccountInterfaceError;
pub use miden_standards::errors::CodeBuilderError;
pub use miden_tx::AuthenticationError;
use miden_tx::utils::HexParseError;
use miden_tx::utils::serde::DeserializationError;
use miden_tx::{
DataStoreError,
NoteCheckerError,
TransactionExecutorError,
TransactionProverError,
};
use thiserror::Error;
use crate::note::NoteScreenerError;
use crate::note_transport::NoteTransportError;
use crate::rpc::RpcError;
use crate::store::{NoteRecordError, StoreError};
use crate::transaction::{BatchBuilderError, TransactionRequestError, TransactionStoreUpdateError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorHint {
message: String,
docs_url: Option<&'static str>,
}
impl ErrorHint {
pub fn into_help_message(self) -> String {
self.to_string()
}
}
impl fmt::Display for ErrorHint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.docs_url {
Some(url) => write!(f, "{} See docs: {}", self.message, url),
None => f.write_str(self.message.as_str()),
}
}
}
const TROUBLESHOOTING_DOC: &str =
"https://docs.miden.xyz/builder/tools/clients/rust-client/cli/cli-troubleshooting";
#[derive(Debug, Error)]
pub enum ClientError {
#[error("address {0} is already being tracked")]
AddressAlreadyTracked(String),
#[error("account with id {0} is already being tracked")]
AccountAlreadyTracked(AccountId),
#[error("account error")]
AccountError(#[from] AccountError),
#[error("account {0} is locked because the local state may be out of date with the network")]
AccountLocked(AccountId),
#[error(
"account import failed: the on-chain account commitment ({0}) does not match the commitment of the account being imported"
)]
AccountCommitmentMismatch(Word),
#[error("account {0} is private and its details cannot be retrieved from the network")]
AccountIsPrivate(AccountId),
#[error("account {0} is watched and cannot be used to execute transactions")]
AccountIsWatched(AccountId),
#[error(
"account {0} is already tracked with a different ClientAccountType; switching between Native and Watched is not supported"
)]
AccountWatchedMismatch(AccountId),
#[error("account with id {0} not found on the network")]
AccountNotFoundOnChain(AccountId),
#[error(
"cannot import account: the local account nonce is higher than the imported one, meaning the local state is newer"
)]
AccountNonceTooLow,
#[error("asset error")]
AssetError(#[from] AssetError),
#[error("account data wasn't found for account id {0}")]
AccountDataNotFound(AccountId),
#[error(transparent)]
BatchBuilder(#[from] BatchBuilderError),
#[error("data store error")]
DataStoreError(#[from] DataStoreError),
#[error("failed to construct the partial blockchain")]
PartialBlockchainError(#[from] PartialBlockchainError),
#[error("failed to build proposed batch")]
ProposedBatchError(#[from] ProposedBatchError),
#[error("failed to prove batch")]
ProvenBatchError(#[from] ProvenBatchError),
#[error("failed to deserialize data")]
DataDeserializationError(#[from] DeserializationError),
#[error("note with id {0} not found on chain")]
NoteNotFoundOnChain(NoteId),
#[error("failed to parse hex string")]
HexParseError(#[from] HexParseError),
#[error(
"the chain Merkle Mountain Range (MMR) forest value exceeds the supported range (must fit in a u32)"
)]
InvalidPartialMmrForest,
#[error("chain validation error: {0}")]
ChainValidationError(String),
#[error(
"cannot track a new account without its seed; the seed is required to validate the account ID's correctness"
)]
AddNewAccountWithoutSeed,
#[error("merkle proof error")]
MerkleError(#[from] MerkleError),
#[error(
"transaction output mismatch: expected output notes with recipient digests {0:?} were not produced by the transaction"
)]
MissingOutputRecipients(Vec<Word>),
#[error("note error")]
NoteError(#[from] NoteError),
#[error("note consumption check failed")]
NoteCheckerError(#[from] NoteCheckerError),
#[error("note import error: {0}")]
NoteImportError(String),
#[error("failed to convert note record")]
NoteRecordConversionError(#[from] NoteRecordError),
#[error("note transport error")]
NoteTransportError(#[from] NoteTransportError),
#[error(
"account {0} has no notes available to consume; sync the client or check that notes targeting this account exist"
)]
NoConsumableNoteForAccount(AccountId),
#[error("RPC error")]
RpcError(#[from] RpcError),
#[error(
"transaction failed a recency check: {0} — the reference block may be too old; try syncing and resubmitting"
)]
RecencyConditionError(&'static str),
#[error("note relevance check failed")]
NoteScreenerError(#[from] NoteScreenerError),
#[error("storage error")]
StoreError(#[from] StoreError),
#[error("transaction execution failed")]
TransactionExecutorError(#[from] TransactionExecutorError),
#[error("invalid transaction input")]
TransactionInputError(#[source] TransactionInputError),
#[error("transaction proving failed")]
TransactionProvingError(#[from] TransactionProverError),
#[error("invalid transaction request")]
TransactionRequestError(#[from] TransactionRequestError),
#[error("failed to build transaction script from account interface")]
AccountInterfaceError(#[from] AccountInterfaceError),
#[error("transaction script error")]
TransactionScriptError(#[source] TransactionScriptError),
#[error("client initialization error: {0}")]
ClientInitializationError(String),
#[error("expected full account data for account {0}, but only partial data is available")]
AccountRecordNotFull(AccountId),
#[error("expected partial account data for account {0}, but full data was found")]
AccountRecordNotPartial(AccountId),
#[error("failed to register NTX note script with root {script_root:?}")]
NtxScriptRegistrationFailed {
script_root: Word,
#[source]
source: RpcError,
},
#[error(
"transaction {} was accepted into the node's mempool at block {} but the local store \
update failed. The pending store update is attached and can be re-applied later via \
`apply_transaction_update`. Resubmitting the same transaction will be rejected if the \
original is still in the mempool or has been finalized in a block, because the \
account (and network) state has already been mutated by the accepted copy.",
pending_update.executed_transaction().id(),
pending_update.submission_height()
)]
ApplyTransactionAfterSubmitFailed {
pending_update: Box<crate::transaction::TransactionStoreUpdate>,
#[source]
source: Box<ClientError>,
},
}
impl From<ClientError> for String {
fn from(err: ClientError) -> String {
err.to_string()
}
}
impl From<TransactionStoreUpdateError> for ClientError {
fn from(err: TransactionStoreUpdateError) -> Self {
match err {
TransactionStoreUpdateError::Store(e) => ClientError::StoreError(e),
TransactionStoreUpdateError::NoteScreener(e) => ClientError::NoteScreenerError(e),
TransactionStoreUpdateError::NoteRecord(e) => ClientError::NoteRecordConversionError(e),
}
}
}
impl From<&ClientError> for Option<ErrorHint> {
fn from(err: &ClientError) -> Self {
match err {
ClientError::MissingOutputRecipients(recipients) => {
Some(missing_recipient_hint(recipients))
},
ClientError::TransactionRequestError(inner) => inner.into(),
ClientError::TransactionExecutorError(inner) => transaction_executor_hint(inner),
ClientError::NoteNotFoundOnChain(note_id) => Some(ErrorHint {
message: format!(
"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."
),
docs_url: Some(TROUBLESHOOTING_DOC),
}),
ClientError::AccountLocked(account_id) => Some(ErrorHint {
message: format!(
"Account {account_id} is locked because the client may be missing its latest \
state. This can happen when the account is shared and another client executed \
a transaction. Run `sync` to fetch the latest state from the network."
),
docs_url: Some(TROUBLESHOOTING_DOC),
}),
ClientError::AccountNonceTooLow => Some(ErrorHint {
message: "The account you are trying to import has an older nonce than the version \
already tracked locally. Run `sync` to ensure your local state is current, \
or re-export the account from a more up-to-date source.".to_string(),
docs_url: Some(TROUBLESHOOTING_DOC),
}),
ClientError::NoConsumableNoteForAccount(account_id) => Some(ErrorHint {
message: format!(
"No notes were found that account {account_id} can consume. \
Run `sync` to fetch the latest notes from the network, \
and verify that notes targeting this account have been committed on chain."
),
docs_url: Some(TROUBLESHOOTING_DOC),
}),
ClientError::RpcError(RpcError::ConnectionError(_)) => Some(ErrorHint {
message: "Could not reach the Miden node. Check that the node endpoint in your \
configuration is correct and that the node is running.".to_string(),
docs_url: Some(TROUBLESHOOTING_DOC),
}),
ClientError::RpcError(RpcError::AcceptHeaderError(_)) => Some(ErrorHint {
message: "The node rejected the request due to a version mismatch. \
Ensure your client version is compatible with the node version.".to_string(),
docs_url: Some(TROUBLESHOOTING_DOC),
}),
ClientError::AddNewAccountWithoutSeed => Some(ErrorHint {
message: "New accounts require a seed to derive their initial state. \
Use `Client::new_account()` which generates the seed automatically, \
or provide the seed when importing.".to_string(),
docs_url: Some(TROUBLESHOOTING_DOC),
}),
ClientError::ApplyTransactionAfterSubmitFailed { pending_update, .. } => {
let tx_id = pending_update.executed_transaction().id();
let submission_height = pending_update.submission_height();
Some(ErrorHint {
message: format!(
"Transaction {tx_id} was accepted into the node's mempool at block \
{submission_height} but the local store update failed. The pending \
update is attached to this error as `pending_update`; you can re-apply \
it later via `Client::apply_transaction_update`. Do NOT resubmit the \
same transaction: if the original is still in the mempool or has been \
finalized in a block, the account (and network) state has already been \
mutated by the accepted copy, so the node will reject the retry."
),
docs_url: Some(TROUBLESHOOTING_DOC),
})
},
_ => None,
}
}
}
impl ClientError {
pub fn error_hint(&self) -> Option<ErrorHint> {
self.into()
}
}
impl From<&TransactionRequestError> for Option<ErrorHint> {
fn from(err: &TransactionRequestError) -> Self {
match err {
TransactionRequestError::NoInputNotesNorAccountChange => Some(ErrorHint {
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(),
docs_url: Some(TROUBLESHOOTING_DOC),
}),
TransactionRequestError::StorageSlotNotFound(slot, account_id) => {
Some(storage_miss_hint(*slot, *account_id))
},
TransactionRequestError::InputNoteNotAuthenticated(note_id) => Some(ErrorHint {
message: format!(
"Note {note_id} needs an inclusion proof before it can be consumed as an \
authenticated input. Run `sync` to fetch the latest proofs from the network."
),
docs_url: Some(TROUBLESHOOTING_DOC),
}),
TransactionRequestError::P2IDNoteWithoutAsset => Some(ErrorHint {
message: "A pay-to-ID (P2ID) note transfers assets to a target account. \
Add at least one fungible or non-fungible asset to the note.".to_string(),
docs_url: Some(TROUBLESHOOTING_DOC),
}),
TransactionRequestError::OutputNoteSenderMismatch { expected, actual } => {
Some(ErrorHint {
message: format!(
"A note's sender is the account that emits it: it must be the account \
executing the transaction. This transaction runs as account {expected}, \
but one of its output notes declares sender {actual}. Rebuild the note \
with {expected} as its sender, or execute the transaction from {actual}."
),
docs_url: Some(TROUBLESHOOTING_DOC),
})
},
_ => None,
}
}
}
impl TransactionRequestError {
pub fn error_hint(&self) -> Option<ErrorHint> {
self.into()
}
}
fn missing_recipient_hint(recipients: &[Word]) -> ErrorHint {
let message = format!(
"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."
);
ErrorHint {
message,
docs_url: Some(TROUBLESHOOTING_DOC),
}
}
fn storage_miss_hint(slot: u8, account_id: AccountId) -> ErrorHint {
ErrorHint {
message: format!(
"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."
),
docs_url: Some(TROUBLESHOOTING_DOC),
}
}
fn transaction_executor_hint(err: &TransactionExecutorError) -> Option<ErrorHint> {
match err {
TransactionExecutorError::ForeignAccountNotAnchoredInReference(account_id) => {
Some(ErrorHint {
message: format!(
"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."
),
docs_url: Some(TROUBLESHOOTING_DOC),
})
},
TransactionExecutorError::TransactionProgramExecutionFailed(_) => Some(ErrorHint {
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(),
docs_url: Some(TROUBLESHOOTING_DOC),
}),
_ => None,
}
}
#[derive(Debug, Error)]
pub enum IdPrefixFetchError {
#[error("no stored notes matched the provided prefix '{0}'")]
NoMatch(String),
#[error(
"multiple {0} entries match the provided prefix; provide a longer prefix to narrow it down"
)]
MultipleMatches(String),
}