use dapi_grpc::tonic::Code;
use dpp::consensus::ConsensusError;
use dpp::serialization::PlatformDeserializable;
use dpp::version::PlatformVersionError;
use dpp::ProtocolError;
pub use drive_proof_verifier::error::ContextProviderError;
use rs_dapi_client::transport::TransportError;
use rs_dapi_client::{CanRetry, DapiClientError, ExecutionError};
use std::fmt::Debug;
use std::time::Duration;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("SDK misconfigured: {0}")]
Config(String),
#[error("Drive error: {0}")]
Drive(#[from] drive::error::Error),
#[error("Protocol error: {0}")]
Protocol(#[from] ProtocolError),
#[error("Proof verification error: {0}")]
Proof(#[from] drive_proof_verifier::Error),
#[error("Invalid Proved Response error: {0}")]
InvalidProvedResponse(String),
#[error("Dapi client error: {0}")]
DapiClientError(String),
#[cfg(feature = "mocks")]
#[error("Dapi mocks error: {0}")]
DapiMocksError(#[from] rs_dapi_client::mock::MockError),
#[error("Dash core error: {0}")]
CoreError(#[from] dpp::dashcore::Error),
#[error("Dash core error: {0}")]
MerkleBlockError(#[from] dpp::dashcore::merkle_tree::MerkleBlockError),
#[error("Core client error: {0}")]
CoreClientError(#[from] dashcore_rpc::Error),
#[error("Required {0} not found: {1}")]
MissingDependency(String, String),
#[error("Total credits in Platform are not found; it should never happen")]
TotalCreditsNotFound,
#[error("No epoch found on Platform; it should never happen")]
EpochNotFound,
#[error("SDK operation timeout {} secs reached: {1}", .0.as_secs())]
TimeoutReached(Duration, String),
#[error("Object already exists: {0}")]
AlreadyExists(String),
#[error("SDK error: {0}")]
Generic(String),
#[error("Context provider error: {0}")]
ContextProviderError(#[from] ContextProviderError),
#[error("Operation cancelled: {0}")]
Cancelled(String),
#[error(transparent)]
StaleNode(#[from] StaleNodeError),
}
impl From<DapiClientError> for Error {
fn from(value: DapiClientError) -> Self {
if let DapiClientError::Transport(TransportError::Grpc(status)) = &value {
if let Some(consensus_error_value) = status
.metadata()
.get_bin("dash-serialized-consensus-error-bin")
{
return consensus_error_value
.to_bytes()
.map(|bytes| {
ConsensusError::deserialize_from_bytes(&bytes)
.map(|consensus_error| {
Self::Protocol(ProtocolError::ConsensusError(Box::new(
consensus_error,
)))
})
.unwrap_or_else(|e| {
tracing::debug!("Failed to deserialize consensus error: {}", e);
Self::Protocol(e)
})
})
.unwrap_or_else(|e| {
tracing::debug!("Failed to deserialize consensus error: {}", e);
Self::Generic(format!("Invalid consensus error encoding: {e}"))
});
}
if status.code() == Code::AlreadyExists {
return Self::AlreadyExists(status.message().to_string());
}
}
Self::DapiClientError(value.to_string())
}
}
impl From<PlatformVersionError> for Error {
fn from(value: PlatformVersionError) -> Self {
Self::Protocol(value.into())
}
}
impl<T> From<ExecutionError<T>> for Error
where
ExecutionError<T>: ToString,
{
fn from(value: ExecutionError<T>) -> Self {
Self::DapiClientError(value.to_string())
}
}
impl CanRetry for Error {
fn can_retry(&self) -> bool {
matches!(self, Error::StaleNode(..) | Error::TimeoutReached(_, _))
}
}
#[derive(Debug, thiserror::Error)]
pub enum StaleNodeError {
#[error("received height is outdated: expected {expected_height}, received {received_height}, tolerance {tolerance_blocks}; try another server")]
Height {
expected_height: u64,
received_height: u64,
tolerance_blocks: u64,
},
#[error(
"received invalid time: expected {expected_timestamp_ms}ms, received {received_timestamp_ms} ms, tolerance {tolerance_ms} ms; try another server"
)]
Time {
expected_timestamp_ms: u64,
received_timestamp_ms: u64,
tolerance_ms: u64,
},
}
#[cfg(test)]
mod tests {
use super::*;
mod from_dapi_client_error {
use super::*;
use assert_matches::assert_matches;
use base64::Engine;
use dapi_grpc::tonic::metadata::{MetadataMap, MetadataValue};
use dpp::consensus::basic::identity::IdentityAssetLockProofLockedTransactionMismatchError;
use dpp::consensus::basic::BasicError;
use dpp::dashcore::hashes::Hash;
use dpp::dashcore::Txid;
use dpp::serialization::PlatformSerializableWithPlatformVersion;
use dpp::version::PlatformVersion;
#[test]
fn test_already_exists() {
let error = DapiClientError::Transport(TransportError::Grpc(
dapi_grpc::tonic::Status::new(Code::AlreadyExists, "Object already exists"),
));
let sdk_error: Error = error.into();
assert!(matches!(sdk_error, Error::AlreadyExists(_)));
}
#[test]
fn test_consensus_error() {
let platform_version = PlatformVersion::latest();
let consensus_error = ConsensusError::BasicError(
BasicError::IdentityAssetLockProofLockedTransactionMismatchError(
IdentityAssetLockProofLockedTransactionMismatchError::new(
Txid::from_byte_array([0; 32]),
Txid::from_byte_array([1; 32]),
),
),
);
let consensus_error_bytes = consensus_error
.serialize_to_bytes_with_platform_version(platform_version)
.expect("serialize consensus error to bytes");
let mut metadata = MetadataMap::new();
metadata.insert_bin(
"dash-serialized-consensus-error-bin",
MetadataValue::from_bytes(&consensus_error_bytes),
);
let status =
dapi_grpc::tonic::Status::with_metadata(Code::InvalidArgument, "Test", metadata);
let error = DapiClientError::Transport(TransportError::Grpc(status));
let sdk_error = Error::from(error);
assert_matches!(
sdk_error,
Error::Protocol(ProtocolError::ConsensusError(e)) if matches!(*e, ConsensusError::BasicError(
BasicError::IdentityAssetLockProofLockedTransactionMismatchError(_)
))
);
}
#[test]
fn test_consensus_error_with_fixture() {
let consensus_error_bytes = base64::engine::general_purpose::STANDARD.decode("ATUgJOJEYbuHBqyTeApO/ptxQ8IAw8nm9NbGROu1nyE/kqcgDTlFeUG0R4wwVcbZJMFErL+VSn63SUpP49cequ3fsKw=").expect("decode base64");
let consensus_error = MetadataValue::from_bytes(&consensus_error_bytes);
let mut metadata = MetadataMap::new();
metadata.insert_bin("dash-serialized-consensus-error-bin", consensus_error);
let status =
dapi_grpc::tonic::Status::with_metadata(Code::InvalidArgument, "Test", metadata);
let error = DapiClientError::Transport(TransportError::Grpc(status));
let sdk_error = Error::from(error);
assert_matches!(
sdk_error,
Error::Protocol(ProtocolError::ConsensusError(e)) if matches!(*e, ConsensusError::BasicError(
BasicError::IdentityAssetLockProofLockedTransactionMismatchError(_)
))
);
}
}
}