use miden_client::rpc::{GrpcError, RpcError};
use miden_protocol::account::AccountId;
use miden_protocol::note::NoteId;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, MultisigError>;
#[derive(Debug, Error)]
pub enum MultisigError {
#[error("account not found: {0}")]
AccountNotFound(AccountId),
#[error("proposal not found: {0}")]
ProposalNotFound(String),
#[error("GUARDIAN connection error: {0}")]
GuardianConnection(String),
#[error("GUARDIAN server error: {0}")]
GuardianServer(String),
#[error("miden client error: {0}")]
MidenClient(String),
#[error("miden client error: {message}")]
MidenClientSource {
message: String,
#[source]
source: Box<miden_client::ClientError>,
},
#[error("miden RPC error: {message}")]
MidenRpcSource {
message: String,
#[source]
source: Box<RpcError>,
},
#[error("sync panicked (corrupted local state): {0}")]
SyncPanicked(String),
#[error("transaction execution failed: {0}")]
TransactionExecution(String),
#[error("transaction execution failed: {message}")]
TransactionExecutionSource {
message: String,
#[source]
source: Box<miden_client::ClientError>,
},
#[error("invalid configuration: {0}")]
InvalidConfig(String),
#[error("invalid prover URL: {0}")]
InvalidProverUrl(String),
#[error("signature error: {0}")]
Signature(String),
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("not a cosigner for this account")]
NotCosigner,
#[error("already signed this proposal")]
AlreadySigned,
#[error("proposal not ready: need {required} signatures, have {collected}")]
ProposalNotReady { required: usize, collected: usize },
#[error("signer not configured")]
NoSigner,
#[error("missing required configuration: {0}")]
MissingConfig(String),
#[error("hex decode error: {0}")]
HexDecode(String),
#[error("account storage error: {0}")]
AccountStorage(String),
#[error("transaction executed successfully when failure was expected")]
UnexpectedSuccess,
#[error("unknown transaction type: {0}")]
UnknownTransactionType(String),
#[error("unsupported transaction type for this operation: {0}")]
UnsupportedTransactionType(String),
#[error("invalid filter: {0}")]
InvalidFilter(String),
#[error("offline mode only supports SwitchGuardian transactions, got: {0}")]
OfflineUnsupportedTransaction(String),
#[error("consume_notes metadata note binding mismatch: {0}")]
NoteBindingMismatch(String),
#[error("unsupported consume_notes metadata version: {found:?}")]
UnsupportedMetadataVersion { found: Option<u32> },
#[error(
"consume_notes metadata exceeds size limit: limit={limit} bytes, actual={actual} bytes"
)]
ConsumeNotesMetadataOversize { limit: usize, actual: usize },
#[error("consume_notes legacy verification: note not found in local store: {note_id}")]
LegacyConsumeNotesNoteMissing { note_id: NoteId },
}
impl MultisigError {
pub fn code(&self) -> Option<&'static str> {
match self {
Self::NoteBindingMismatch(_) => Some("consume_notes_note_binding_mismatch"),
Self::UnsupportedMetadataVersion { .. } => {
Some("consume_notes_unsupported_metadata_version")
}
Self::ConsumeNotesMetadataOversize { .. } => Some("consume_notes_metadata_oversize"),
Self::LegacyConsumeNotesNoteMissing { .. } => Some("consume_notes_legacy_note_missing"),
Self::UnsupportedTransactionType(_) => Some("unsupported_transaction_type"),
_ => None,
}
}
pub(crate) fn miden_client_with_context(
context: impl AsRef<str>,
err: miden_client::ClientError,
) -> Self {
MultisigError::MidenClientSource {
message: format!("{}: {}", context.as_ref(), error_chain(&err)),
source: Box::new(err),
}
}
pub(crate) fn miden_rpc_with_context(context: impl AsRef<str>, err: RpcError) -> Self {
MultisigError::MidenRpcSource {
message: format!("{}: {}", context.as_ref(), error_chain(&err)),
source: Box::new(err),
}
}
pub(crate) fn transaction_execution_with_context(
context: impl AsRef<str>,
err: miden_client::ClientError,
) -> Self {
MultisigError::TransactionExecutionSource {
message: format!("{}: {}", context.as_ref(), error_chain(&err)),
source: Box::new(err),
}
}
pub fn miden_rpc_kind(&self) -> Option<&GrpcError> {
match self {
Self::MidenClientSource { source, .. } => rpc_kind_from_client_error(source),
Self::MidenRpcSource { source, .. } => rpc_kind(source),
Self::TransactionExecutionSource { source, .. } => rpc_kind_from_client_error(source),
_ => None,
}
}
}
fn rpc_kind_from_client_error(error: &miden_client::ClientError) -> Option<&GrpcError> {
match error {
miden_client::ClientError::RpcError(error) => rpc_kind(error),
miden_client::ClientError::ApplyTransactionAfterSubmitFailed { source, .. } => {
rpc_kind_from_client_error(source)
}
_ => None,
}
}
pub(crate) fn rpc_kind(error: &RpcError) -> Option<&GrpcError> {
match error {
RpcError::RequestError { error_kind, .. } => Some(error_kind),
_ => None,
}
}
impl From<guardian_client::ClientError> for MultisigError {
fn from(err: guardian_client::ClientError) -> Self {
MultisigError::GuardianServer(err.to_string())
}
}
pub(crate) fn error_chain(err: &dyn std::error::Error) -> String {
let mut message = err.to_string();
let mut source = err.source();
while let Some(cause) = source {
message.push_str(": ");
message.push_str(&cause.to_string());
source = cause.source();
}
message
}
impl From<miden_client::ClientError> for MultisigError {
fn from(err: miden_client::ClientError) -> Self {
MultisigError::MidenClientSource {
message: error_chain(&err),
source: Box::new(err),
}
}
}
impl From<miden_client::transaction::TransactionRequestError> for MultisigError {
fn from(err: miden_client::transaction::TransactionRequestError) -> Self {
MultisigError::TransactionExecution(err.to_string())
}
}
impl From<miden_client::transaction::TransactionExecutorError> for MultisigError {
fn from(err: miden_client::transaction::TransactionExecutorError) -> Self {
MultisigError::TransactionExecution(err.to_string())
}
}
#[cfg(test)]
mod tests {
use miden_client::rpc::RpcEndpoint;
use super::*;
pub(crate) fn request_error(kind: GrpcError) -> miden_client::ClientError {
RpcError::RequestError {
endpoint: RpcEndpoint::SyncChainMmr,
error_kind: kind,
endpoint_error: None,
source: None,
}
.into()
}
#[test]
fn miden_client_conversion_retains_typed_rpc_kind() {
let error = MultisigError::from(request_error(GrpcError::ResourceExhausted));
assert!(matches!(
error.miden_rpc_kind(),
Some(GrpcError::ResourceExhausted)
));
assert!(error.to_string().contains("sync_chain_mmr"));
}
#[test]
fn context_wrapper_keeps_call_site_detail_and_typed_source() {
let error = MultisigError::miden_client_with_context(
"failed to fetch on-chain commitment for account 0xabc",
request_error(GrpcError::Unavailable),
);
assert!(error.to_string().contains("account 0xabc"));
assert!(error.to_string().contains("sync_chain_mmr"));
assert!(matches!(
error.miden_rpc_kind(),
Some(GrpcError::Unavailable)
));
}
#[test]
fn errors_without_rpc_origin_have_no_rpc_kind() {
assert!(
MultisigError::GuardianServer("There's already a pending change".to_string())
.miden_rpc_kind()
.is_none()
);
assert!(
MultisigError::MidenClient("account not found on chain".to_string())
.miden_rpc_kind()
.is_none()
);
}
#[test]
fn rpc_context_wrapper_keeps_call_site_detail_and_typed_source() {
let error = MultisigError::miden_rpc_with_context(
"failed to fetch on-chain commitment for account 0xabc",
RpcError::RequestError {
endpoint: RpcEndpoint::GetAccount,
error_kind: GrpcError::ResourceExhausted,
endpoint_error: None,
source: None,
},
);
assert!(error.to_string().contains("account 0xabc"));
assert!(error.to_string().contains("get_account"));
assert!(matches!(
error.miden_rpc_kind(),
Some(GrpcError::ResourceExhausted)
));
}
#[test]
fn execution_context_wrapper_keeps_typed_source() {
let error = MultisigError::transaction_execution_with_context(
"transaction submission failed",
request_error(GrpcError::Unavailable),
);
assert!(error.to_string().contains("transaction submission failed"));
assert!(error.to_string().contains("sync_chain_mmr"));
assert!(matches!(
error.miden_rpc_kind(),
Some(GrpcError::Unavailable)
));
}
}