1use 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#[allow(clippy::large_enum_variant)]
23#[derive(Debug, thiserror::Error)]
24pub enum Error {
25 #[error("SDK misconfigured: {0}")]
27 Config(String),
28 #[error("Drive error: {0}")]
30 Drive(#[from] drive::error::Error),
31 #[error("Drive error with associated proof: {0}")]
33 DriveProofError(drive::error::proof::ProofError, Vec<u8>, BlockInfo),
34 #[error("Protocol error: {0}")]
36 Protocol(#[from] ProtocolError),
37 #[error("Proof verification error: {0}")]
39 Proof(#[from] drive_proof_verifier::Error),
40 #[error("Invalid Proved Response error: {0}")]
42 InvalidProvedResponse(String),
43 #[error("Dapi client error: {0}")]
45 DapiClientError(rs_dapi_client::DapiClientError),
46 #[cfg(feature = "mocks")]
47 #[error("Dapi mocks error: {0}")]
49 DapiMocksError(#[from] rs_dapi_client::mock::MockError),
50 #[error("Dash core error: {0}")]
52 CoreError(#[from] dpp::dashcore::Error),
53 #[error("Dash core error: {0}")]
55 MerkleBlockError(#[from] dpp::dashcore::merkle_tree::MerkleBlockError),
56 #[error("Core client error: {0}")]
58 CoreClientError(#[from] dashcore_rpc::Error),
59 #[error("Required {0} not found: {1}")]
61 MissingDependency(String, String),
62 #[error("Total credits in Platform are not found; it should never happen")]
64 TotalCreditsNotFound,
65 #[error("No epoch found on Platform; it should never happen")]
67 EpochNotFound,
68 #[error("SDK operation timeout {} secs reached: {}", .0.as_secs(), .1)]
70 TimeoutReached(Duration, String),
71
72 #[error("Object already exists: {0}")]
74 AlreadyExists(String),
75 #[error("Invalid credit transfer: {0}")]
77 InvalidCreditTransfer(String),
78 #[error("Identity nonce overflow: nonce has reached the maximum value ({0})")]
81 NonceOverflow(u64),
82 #[error("Identity nonce not found on platform: {0}")]
92 IdentityNonceNotFound(String),
93
94 #[error("SDK error: {0}")]
97 Generic(String),
98
99 #[error("Context provider error: {0}")]
101 ContextProviderError(#[from] ContextProviderError),
102
103 #[error("Operation cancelled: {0}")]
105 Cancelled(String),
106
107 #[error(transparent)]
109 StaleNode(#[from] StaleNodeError),
110
111 #[error(transparent)]
113 StateTransitionBroadcastError(#[from] StateTransitionBroadcastError),
114
115 #[error("no available addresses to retry, last error: {0}")]
118 NoAvailableAddressesToRetry(Box<Error>),
119}
120
121#[derive(Debug, thiserror::Error)]
123#[error("state transition broadcast error: {message}")]
124pub struct StateTransitionBroadcastError {
125 pub code: u32,
127 pub message: String,
129 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
158impl From<DapiClientError> for Error {
160 fn from(value: DapiClientError) -> Self {
161 if let DapiClientError::Transport(TransportError::Grpc(status)) = &value {
162 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 Self::Generic(format!("Invalid consensus error encoding: {e}"))
185 });
186 }
187 if status.code() == Code::AlreadyExists {
189 return Self::AlreadyExists(status.message().to_string());
190 }
191 }
192
193 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
255impl<T> From<ExecutionError<T>> for Error
257where
258 ExecutionError<T>: ToString,
259{
260 fn from(value: ExecutionError<T>) -> Self {
261 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#[derive(Debug, thiserror::Error)]
285pub enum StaleNodeError {
286 #[error("received height is outdated: expected {expected_height}, received {received_height}, tolerance {tolerance_blocks}; try another server")]
288 Height {
289 expected_height: u64,
291 received_height: u64,
293 tolerance_blocks: u64,
295 },
296 #[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_timestamp_ms: u64,
303 received_timestamp_ms: u64,
305 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}