dash_sdk/
error.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
//! Definitions of errors
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;

/// Error type for the SDK
// TODO: Propagate server address and retry information so that the user can retrieve it
#[derive(Debug, thiserror::Error)]
pub enum Error {
    /// SDK is not configured properly
    #[error("SDK misconfigured: {0}")]
    Config(String),
    /// Drive error
    #[error("Drive error: {0}")]
    Drive(#[from] drive::error::Error),
    /// DPP error
    #[error("Protocol error: {0}")]
    Protocol(#[from] ProtocolError),
    /// Proof verification error
    #[error("Proof verification error: {0}")]
    Proof(#[from] drive_proof_verifier::Error),
    /// Invalid Proved Response error
    #[error("Invalid Proved Response error: {0}")]
    InvalidProvedResponse(String),
    /// DAPI client error, for example, connection error
    #[error("Dapi client error: {0}")]
    DapiClientError(String),
    #[cfg(feature = "mocks")]
    /// DAPI mocks error
    #[error("Dapi mocks error: {0}")]
    DapiMocksError(#[from] rs_dapi_client::mock::MockError),
    /// Dash core error
    #[error("Dash core error: {0}")]
    CoreError(#[from] dpp::dashcore::Error),
    /// MerkleBlockError
    #[error("Dash core error: {0}")]
    MerkleBlockError(#[from] dpp::dashcore::merkle_tree::MerkleBlockError),
    /// Core client error, for example, connection error
    #[error("Core client error: {0}")]
    CoreClientError(#[from] dashcore_rpc::Error),
    /// Dependency not found, for example data contract for a document not found
    #[error("Required {0} not found: {1}")]
    MissingDependency(String, String),
    /// Total credits in Platform are not found; we must always have credits in Platform
    #[error("Total credits in Platform are not found; it should never happen")]
    TotalCreditsNotFound,
    /// Epoch not found; we must have at least one epoch
    #[error("No epoch found on Platform; it should never happen")]
    EpochNotFound,
    /// SDK operation timeout reached error
    #[error("SDK operation timeout {} secs reached: {1}", .0.as_secs())]
    TimeoutReached(Duration, String),

    /// Returned when an attempt is made to create an object that already exists in the system
    #[error("Object already exists: {0}")]
    AlreadyExists(String),
    /// Generic error
    // TODO: Use domain specific errors instead of generic ones
    #[error("SDK error: {0}")]
    Generic(String),

    /// Context provider error
    #[error("Context provider error: {0}")]
    ContextProviderError(#[from] ContextProviderError),

    /// Operation cancelled - cancel token was triggered, timeout, etc.
    #[error("Operation cancelled: {0}")]
    Cancelled(String),

    /// Remote node is stale; try another server
    #[error(transparent)]
    StaleNode(#[from] StaleNodeError),
}

// TODO: Decompose DapiClientError to more specific errors like connection, node error instead of DAPI client error
impl From<DapiClientError> for Error {
    fn from(value: DapiClientError) -> Self {
        if let DapiClientError::Transport(TransportError::Grpc(status)) = &value {
            // If we have some consensus error metadata, we deserialize it and return as ConsensusError
            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);
                        // TODO: Introduce a specific error for this case
                        Self::Generic(format!("Invalid consensus error encoding: {e}"))
                    });
            }
            // Otherwise we parse the error code and act accordingly
            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 {
        // TODO: Improve error handling
        Self::DapiClientError(value.to_string())
    }
}

impl CanRetry for Error {
    fn can_retry(&self) -> bool {
        matches!(self, Error::StaleNode(..) | Error::TimeoutReached(_, _))
    }
}

/// Server returned stale metadata
#[derive(Debug, thiserror::Error)]
pub enum StaleNodeError {
    /// Server returned metadata with outdated height
    #[error("received height is outdated: expected {expected_height}, received {received_height}, tolerance {tolerance_blocks}; try another server")]
    Height {
        /// Expected height - last block height seen by the Sdk
        expected_height: u64,
        /// Block height received from the server
        received_height: u64,
        /// Tolerance - how many blocks can be behind the expected height
        tolerance_blocks: u64,
    },
    /// Server returned metadata with time outside of the tolerance
    #[error(
        "received invalid time: expected {expected_timestamp_ms}ms, received {received_timestamp_ms} ms, tolerance {tolerance_ms} ms; try another server"
    )]
    Time {
        /// Expected time in milliseconds - is local time when the message was received
        expected_timestamp_ms: u64,
        /// Time received from the server in the message, in milliseconds
        received_timestamp_ms: u64,
        /// Tolerance in milliseconds
        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(_)
                ))
            );
        }
    }
}