dash_sdk/
error.rs

1//! Definitions of errors
2use dapi_grpc::platform::v0::StateTransitionBroadcastError as StateTransitionBroadcastErrorProto;
3use dapi_grpc::tonic::Code;
4pub use dash_context_provider::ContextProviderError;
5use dpp::block::block_info::BlockInfo;
6use dpp::consensus::basic::state_transition::{
7    OutputBelowMinimumError, TransitionNoInputsError, TransitionNoOutputsError,
8};
9use dpp::consensus::state::address_funds::{AddressDoesNotExistError, AddressNotEnoughFundsError};
10use dpp::consensus::ConsensusError;
11use dpp::serialization::PlatformDeserializable;
12use dpp::validation::SimpleConsensusValidationResult;
13use dpp::version::PlatformVersionError;
14use dpp::{dashcore_rpc, ProtocolError};
15use rs_dapi_client::transport::TransportError;
16use rs_dapi_client::{CanRetry, DapiClientError, ExecutionError};
17use std::fmt::Debug;
18use std::time::Duration;
19
20/// Error type for the SDK
21// TODO: Propagate server address and retry information so that the user can retrieve it
22#[allow(clippy::large_enum_variant)]
23#[derive(Debug, thiserror::Error)]
24pub enum Error {
25    /// SDK is not configured properly
26    #[error("SDK misconfigured: {0}")]
27    Config(String),
28    /// Drive error
29    #[error("Drive error: {0}")]
30    Drive(#[from] drive::error::Error),
31    /// Drive proof error with associated proof bytes and block info
32    #[error("Drive error with associated proof: {0}")]
33    DriveProofError(drive::error::proof::ProofError, Vec<u8>, BlockInfo),
34    /// DPP error
35    #[error("Protocol error: {0}")]
36    Protocol(#[from] ProtocolError),
37    /// Proof verification error
38    #[error("Proof verification error: {0}")]
39    Proof(#[from] drive_proof_verifier::Error),
40    /// Invalid Proved Response error
41    #[error("Invalid Proved Response error: {0}")]
42    InvalidProvedResponse(String),
43    /// DAPI client error, for example, connection error
44    #[error("Dapi client error: {0}")]
45    DapiClientError(rs_dapi_client::DapiClientError),
46    #[cfg(feature = "mocks")]
47    /// DAPI mocks error
48    #[error("Dapi mocks error: {0}")]
49    DapiMocksError(#[from] rs_dapi_client::mock::MockError),
50    /// Dash core error
51    #[error("Dash core error: {0}")]
52    CoreError(#[from] dpp::dashcore::Error),
53    /// MerkleBlockError
54    #[error("Dash core error: {0}")]
55    MerkleBlockError(#[from] dpp::dashcore::merkle_tree::MerkleBlockError),
56    /// Core client error, for example, connection error
57    #[error("Core client error: {0}")]
58    CoreClientError(#[from] dashcore_rpc::Error),
59    /// Dependency not found, for example data contract for a document not found
60    #[error("Required {0} not found: {1}")]
61    MissingDependency(String, String),
62    /// Total credits in Platform are not found; we must always have credits in Platform
63    #[error("Total credits in Platform are not found; it should never happen")]
64    TotalCreditsNotFound,
65    /// Epoch not found; we must have at least one epoch
66    #[error("No epoch found on Platform; it should never happen")]
67    EpochNotFound,
68    /// SDK operation timeout reached error
69    #[error("SDK operation timeout {} secs reached: {}", .0.as_secs(), .1)]
70    TimeoutReached(Duration, String),
71
72    /// Returned when an attempt is made to create an object that already exists in the system
73    #[error("Object already exists: {0}")]
74    AlreadyExists(String),
75    /// Invalid credit transfer configuration
76    #[error("Invalid credit transfer: {0}")]
77    InvalidCreditTransfer(String),
78    /// Identity nonce overflow: the nonce has reached its maximum value and
79    /// cannot be incremented further without wrapping to zero.
80    #[error("Identity nonce overflow: nonce has reached the maximum value ({0})")]
81    NonceOverflow(u64),
82    /// Identity nonce not found on Platform.
83    ///
84    /// Platform returned no nonce for the requested identity (or identity–
85    /// contract pair).  This usually means the queried DAPI node has not yet
86    /// indexed the identity — for example right after identity creation or
87    /// when the node is lagging behind the chain tip.
88    ///
89    /// **Recovery**: retry the state transition; the SDK will re-fetch the
90    /// nonce from a (potentially different) DAPI node on the next attempt.
91    #[error("Identity nonce not found on platform: {0}")]
92    IdentityNonceNotFound(String),
93
94    /// Generic error
95    // TODO: Use domain specific errors instead of generic ones
96    #[error("SDK error: {0}")]
97    Generic(String),
98
99    /// Context provider error
100    #[error("Context provider error: {0}")]
101    ContextProviderError(#[from] ContextProviderError),
102
103    /// Operation cancelled - cancel token was triggered, timeout, etc.
104    #[error("Operation cancelled: {0}")]
105    Cancelled(String),
106
107    /// Remote node is stale; try another server
108    #[error(transparent)]
109    StaleNode(#[from] StaleNodeError),
110
111    /// Error returned when trying to broadcast a state transition
112    #[error(transparent)]
113    StateTransitionBroadcastError(#[from] StateTransitionBroadcastError),
114
115    /// All available addresses have been exhausted (banned due to errors).
116    /// Contains the last meaningful error that caused addresses to be banned.
117    #[error("no available addresses to retry, last error: {0}")]
118    NoAvailableAddressesToRetry(Box<Error>),
119}
120
121/// State transition broadcast error
122#[derive(Debug, thiserror::Error)]
123#[error("state transition broadcast error: {message}")]
124pub struct StateTransitionBroadcastError {
125    /// Error code
126    pub code: u32,
127    /// Error message
128    pub message: String,
129    /// Consensus error caused the state transition broadcast error
130    pub cause: Option<ConsensusError>,
131}
132
133impl TryFrom<StateTransitionBroadcastErrorProto> for StateTransitionBroadcastError {
134    type Error = Error;
135
136    fn try_from(value: StateTransitionBroadcastErrorProto) -> Result<Self, Self::Error> {
137        let cause = if !value.data.is_empty() {
138            let consensus_error =
139                ConsensusError::deserialize_from_bytes(&value.data).map_err(|e| {
140                    tracing::debug!("Failed to deserialize consensus error: {}", e);
141
142                    Error::Protocol(e)
143                })?;
144
145            Some(consensus_error)
146        } else {
147            None
148        };
149
150        Ok(Self {
151            code: value.code,
152            message: value.message,
153            cause,
154        })
155    }
156}
157
158// TODO: Decompose DapiClientError to more specific errors like connection, node error instead of DAPI client error
159impl From<DapiClientError> for Error {
160    fn from(value: DapiClientError) -> Self {
161        if let DapiClientError::Transport(TransportError::Grpc(status)) = &value {
162            // If we have some consensus error metadata, we deserialize it and return as ConsensusError
163            if let Some(consensus_error_value) = status
164                .metadata()
165                .get_bin("dash-serialized-consensus-error-bin")
166            {
167                return consensus_error_value
168                    .to_bytes()
169                    .map(|bytes| {
170                        ConsensusError::deserialize_from_bytes(&bytes)
171                            .map(|consensus_error| {
172                                Self::Protocol(ProtocolError::ConsensusError(Box::new(
173                                    consensus_error,
174                                )))
175                            })
176                            .unwrap_or_else(|e| {
177                                tracing::debug!("Failed to deserialize consensus error: {}", e);
178                                Self::Protocol(e)
179                            })
180                    })
181                    .unwrap_or_else(|e| {
182                        tracing::debug!("Failed to deserialize consensus error: {}", e);
183                        // TODO: Introduce a specific error for this case
184                        Self::Generic(format!("Invalid consensus error encoding: {e}"))
185                    });
186            }
187            // Otherwise we parse the error code and act accordingly
188            if status.code() == Code::AlreadyExists {
189                return Self::AlreadyExists(status.message().to_string());
190            }
191        }
192
193        // Preserve the original DAPI client error for structured inspection
194        Self::DapiClientError(value)
195    }
196}
197
198impl From<PlatformVersionError> for Error {
199    fn from(value: PlatformVersionError) -> Self {
200        Self::Protocol(value.into())
201    }
202}
203
204impl From<ConsensusError> for Error {
205    fn from(value: ConsensusError) -> Self {
206        Self::Protocol(ProtocolError::ConsensusError(Box::new(value)))
207    }
208}
209
210impl From<TransitionNoInputsError> for Error {
211    fn from(value: TransitionNoInputsError) -> Self {
212        Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
213    }
214}
215
216impl From<TransitionNoOutputsError> for Error {
217    fn from(value: TransitionNoOutputsError) -> Self {
218        Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
219    }
220}
221
222impl From<OutputBelowMinimumError> for Error {
223    fn from(value: OutputBelowMinimumError) -> Self {
224        Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
225    }
226}
227
228impl From<SimpleConsensusValidationResult> for Error {
229    fn from(value: SimpleConsensusValidationResult) -> Self {
230        value
231            .errors
232            .into_iter()
233            .next()
234            .map(Error::from)
235            .unwrap_or_else(|| {
236                Error::Protocol(ProtocolError::CorruptedCodeExecution(
237                    "state transition structure validation failed without an error".to_string(),
238                ))
239            })
240    }
241}
242
243impl From<AddressDoesNotExistError> for Error {
244    fn from(value: AddressDoesNotExistError) -> Self {
245        Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
246    }
247}
248
249impl From<AddressNotEnoughFundsError> for Error {
250    fn from(value: AddressNotEnoughFundsError) -> Self {
251        Self::Protocol(ProtocolError::ConsensusError(Box::new(value.into())))
252    }
253}
254
255// Retain legacy behavior for generic execution errors that are not DapiClientError
256impl<T> From<ExecutionError<T>> for Error
257where
258    ExecutionError<T>: ToString,
259{
260    fn from(value: ExecutionError<T>) -> Self {
261        // Fallback to a generic string representation
262        Self::Generic(value.to_string())
263    }
264}
265
266impl CanRetry for Error {
267    fn can_retry(&self) -> bool {
268        matches!(
269            self,
270            Error::StaleNode(..) | Error::TimeoutReached(_, _) | Error::Proof(_)
271        )
272    }
273
274    fn is_no_available_addresses(&self) -> bool {
275        matches!(
276            self,
277            Error::DapiClientError(DapiClientError::NoAvailableAddresses)
278                | Error::DapiClientError(DapiClientError::NoAvailableAddressesToRetry(_))
279        )
280    }
281}
282
283/// Server returned stale metadata
284#[derive(Debug, thiserror::Error)]
285pub enum StaleNodeError {
286    /// Server returned metadata with outdated height
287    #[error("received height is outdated: expected {expected_height}, received {received_height}, tolerance {tolerance_blocks}; try another server")]
288    Height {
289        /// Expected height - last block height seen by the Sdk
290        expected_height: u64,
291        /// Block height received from the server
292        received_height: u64,
293        /// Tolerance - how many blocks can be behind the expected height
294        tolerance_blocks: u64,
295    },
296    /// Server returned metadata with time outside of the tolerance
297    #[error(
298        "received invalid time: expected {expected_timestamp_ms}ms, received {received_timestamp_ms} ms, tolerance {tolerance_ms} ms; try another server"
299    )]
300    Time {
301        /// Expected time in milliseconds - is local time when the message was received
302        expected_timestamp_ms: u64,
303        /// Time received from the server in the message, in milliseconds
304        received_timestamp_ms: u64,
305        /// Tolerance in milliseconds
306        tolerance_ms: u64,
307    },
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    mod from_dapi_client_error {
315        use super::*;
316        use assert_matches::assert_matches;
317        use base64::Engine;
318        use dapi_grpc::tonic::metadata::{MetadataMap, MetadataValue};
319        use dpp::consensus::basic::identity::IdentityAssetLockProofLockedTransactionMismatchError;
320        use dpp::consensus::basic::BasicError;
321        use dpp::dashcore::hashes::Hash;
322        use dpp::dashcore::Txid;
323        use dpp::serialization::PlatformSerializableWithPlatformVersion;
324        use dpp::version::PlatformVersion;
325
326        #[test]
327        fn test_already_exists() {
328            let error = DapiClientError::Transport(TransportError::Grpc(
329                dapi_grpc::tonic::Status::new(Code::AlreadyExists, "Object already exists"),
330            ));
331
332            let sdk_error: Error = error.into();
333            assert!(matches!(sdk_error, Error::AlreadyExists(_)));
334        }
335
336        #[test]
337        fn test_consensus_error() {
338            let platform_version = PlatformVersion::latest();
339
340            let consensus_error = ConsensusError::BasicError(
341                BasicError::IdentityAssetLockProofLockedTransactionMismatchError(
342                    IdentityAssetLockProofLockedTransactionMismatchError::new(
343                        Txid::from_byte_array([0; 32]),
344                        Txid::from_byte_array([1; 32]),
345                    ),
346                ),
347            );
348
349            let consensus_error_bytes = consensus_error
350                .serialize_to_bytes_with_platform_version(platform_version)
351                .expect("serialize consensus error to bytes");
352
353            let mut metadata = MetadataMap::new();
354            metadata.insert_bin(
355                "dash-serialized-consensus-error-bin",
356                MetadataValue::from_bytes(&consensus_error_bytes),
357            );
358
359            let status =
360                dapi_grpc::tonic::Status::with_metadata(Code::InvalidArgument, "Test", metadata);
361
362            let error = DapiClientError::Transport(TransportError::Grpc(status));
363
364            let sdk_error = Error::from(error);
365
366            assert_matches!(
367                sdk_error,
368                Error::Protocol(ProtocolError::ConsensusError(e)) if matches!(*e, ConsensusError::BasicError(
369                    BasicError::IdentityAssetLockProofLockedTransactionMismatchError(_)
370                ))
371            );
372        }
373
374        #[test]
375        fn test_consensus_error_with_fixture() {
376            let consensus_error_bytes = base64::engine::general_purpose::STANDARD.decode("ATUgJOJEYbuHBqyTeApO/ptxQ8IAw8nm9NbGROu1nyE/kqcgDTlFeUG0R4wwVcbZJMFErL+VSn63SUpP49cequ3fsKw=").expect("decode base64");
377            let consensus_error = MetadataValue::from_bytes(&consensus_error_bytes);
378
379            let mut metadata = MetadataMap::new();
380            metadata.insert_bin("dash-serialized-consensus-error-bin", consensus_error);
381
382            let status =
383                dapi_grpc::tonic::Status::with_metadata(Code::InvalidArgument, "Test", metadata);
384
385            let error = DapiClientError::Transport(TransportError::Grpc(status));
386
387            let sdk_error = Error::from(error);
388
389            assert_matches!(
390                sdk_error,
391                Error::Protocol(ProtocolError::ConsensusError(e)) if matches!(*e, ConsensusError::BasicError(
392                    BasicError::IdentityAssetLockProofLockedTransactionMismatchError(_)
393                ))
394            );
395        }
396    }
397}