sui_rpc_api/
error.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use sui_types::error::ErrorCategory;
5use tonic::Code;
6
7use crate::proto::google::rpc::{BadRequest, ErrorInfo, RetryInfo};
8pub use sui_rpc::proto::sui::rpc::v2::ErrorReason;
9
10pub type Result<T, E = RpcError> = std::result::Result<T, E>;
11
12/// An error encountered while serving an RPC request.
13///
14/// General error type used by top-level RPC service methods. The main purpose of this error type
15/// is to provide a convenient type for converting between internal errors and a response that
16/// needs to be sent to a calling client.
17#[derive(Debug)]
18pub struct RpcError {
19    code: Code,
20    message: Option<String>,
21    details: Option<Box<ErrorDetails>>,
22}
23
24impl RpcError {
25    pub fn new<T: Into<String>>(code: Code, message: T) -> Self {
26        Self {
27            code,
28            message: Some(message.into()),
29            details: None,
30        }
31    }
32
33    pub fn not_found() -> Self {
34        Self {
35            code: Code::NotFound,
36            message: None,
37            details: None,
38        }
39    }
40
41    pub fn into_status_proto(self) -> crate::proto::google::rpc::Status {
42        crate::proto::google::rpc::Status {
43            code: self.code.into(),
44            message: self.message.unwrap_or_default(),
45            details: self
46                .details
47                .map(ErrorDetails::into_status_details)
48                .unwrap_or_default(),
49        }
50    }
51}
52
53impl From<RpcError> for tonic::Status {
54    fn from(value: RpcError) -> Self {
55        use prost::Message;
56
57        let code = value.code;
58        let status = value.into_status_proto();
59        let details = status.encode_to_vec().into();
60        let message = status.message;
61
62        tonic::Status::with_details(code, message, details)
63    }
64}
65
66impl From<sui_types::storage::error::Error> for RpcError {
67    fn from(value: sui_types::storage::error::Error) -> Self {
68        use sui_types::storage::error::Kind;
69
70        let code = match value.kind() {
71            Kind::Missing => Code::NotFound,
72            _ => Code::Internal,
73        };
74
75        Self {
76            code,
77            message: Some(value.to_string()),
78            details: None,
79        }
80    }
81}
82
83impl From<anyhow::Error> for RpcError {
84    fn from(value: anyhow::Error) -> Self {
85        Self {
86            code: Code::Internal,
87            message: Some(value.to_string()),
88            details: None,
89        }
90    }
91}
92
93impl From<sui_types::sui_sdk_types_conversions::SdkTypeConversionError> for RpcError {
94    fn from(value: sui_types::sui_sdk_types_conversions::SdkTypeConversionError) -> Self {
95        Self {
96            code: Code::Internal,
97            message: Some(value.to_string()),
98            details: None,
99        }
100    }
101}
102
103impl From<bcs::Error> for RpcError {
104    fn from(value: bcs::Error) -> Self {
105        Self {
106            code: Code::Internal,
107            message: Some(value.to_string()),
108            details: None,
109        }
110    }
111}
112
113impl From<sui_types::quorum_driver_types::QuorumDriverError> for RpcError {
114    fn from(error: sui_types::quorum_driver_types::QuorumDriverError) -> Self {
115        use itertools::Itertools;
116        use sui_types::error::SuiErrorKind;
117        use sui_types::quorum_driver_types::QuorumDriverError::*;
118
119        match error {
120            InvalidUserSignature(err) => {
121                let message = {
122                    let err = match err.as_inner() {
123                        SuiErrorKind::UserInputError { error } => error.to_string(),
124                        _ => err.to_string(),
125                    };
126                    format!("Invalid user signature: {err}")
127                };
128
129                RpcError::new(Code::InvalidArgument, message)
130            }
131            QuorumDriverInternalError(err) => RpcError::new(Code::Internal, err.to_string()),
132            ObjectsDoubleUsed { conflicting_txes } => {
133                let new_map = conflicting_txes
134                    .into_iter()
135                    .map(|(digest, (pairs, _))| {
136                        (
137                            digest,
138                            pairs.into_iter().map(|(_, obj_ref)| obj_ref).collect(),
139                        )
140                    })
141                    .collect::<std::collections::BTreeMap<_, Vec<_>>>();
142
143                let message = format!(
144                    "Failed to sign transaction by a quorum of validators because of locked objects. Conflicting Transactions:\n{new_map:#?}",
145                );
146
147                RpcError::new(Code::FailedPrecondition, message)
148            }
149            TimeoutBeforeFinality | FailedWithTransientErrorAfterMaximumAttempts { .. } => {
150                // TODO add a Retry-After header
151                RpcError::new(
152                    Code::Unavailable,
153                    "timed-out before finality could be reached",
154                )
155            }
156            TimeoutBeforeFinalityWithErrors {
157                last_error,
158                attempts,
159                timeout,
160            } => {
161                // TODO add a Retry-After header
162                RpcError::new(
163                    Code::Unavailable,
164                    format!(
165                        "Transaction timed out before finality could be reached. Attempts: {attempts} & timeout: {timeout:?}. Last error: {last_error}"
166                    ),
167                )
168            }
169            NonRecoverableTransactionError { errors } => {
170                let new_errors: Vec<String> = errors
171                    .into_iter()
172                    // sort by total stake, descending, so users see the most prominent one first
173                    .sorted_by(|(_, a, _), (_, b, _)| b.cmp(a))
174                    .filter_map(|(err, _, _)| {
175                        match err.as_inner() {
176                            // Special handling of UserInputError:
177                            // ObjectNotFound and DependentPackageNotFound are considered
178                            // retryable errors but they have different treatment
179                            // in AuthorityAggregator.
180                            // The optimal fix would be to examine if the total stake
181                            // of ObjectNotFound/DependentPackageNotFound exceeds the
182                            // quorum threshold, but it takes a Committee here.
183                            // So, we take an easier route and consider them non-retryable
184                            // at all. Combining this with the sorting above, clients will
185                            // see the dominant error first.
186                            SuiErrorKind::UserInputError { error } => Some(error.to_string()),
187                            _ => {
188                                if err.is_retryable().0 {
189                                    None
190                                } else {
191                                    Some(err.to_string())
192                                }
193                            }
194                        }
195                    })
196                    .collect();
197
198                assert!(
199                    !new_errors.is_empty(),
200                    "NonRecoverableTransactionError should have at least one non-retryable error"
201                );
202
203                let error_list = new_errors.join(", ");
204                let error_msg = format!(
205                    "Transaction execution failed due to issues with transaction inputs, please review the errors and try again: {}.",
206                    error_list
207                );
208
209                RpcError::new(Code::InvalidArgument, error_msg)
210            }
211            TxAlreadyFinalizedWithDifferentUserSignatures => RpcError::new(
212                Code::Aborted,
213                "The transaction is already finalized but with different user signatures",
214            ),
215            SystemOverload { .. } | SystemOverloadRetryAfter { .. } => {
216                // TODO add a Retry-After header
217                RpcError::new(Code::Unavailable, "system is overloaded")
218            }
219            TransactionFailed { category, details } => RpcError::new(
220                // TODO(fastpath): add a Retry-After header.
221                match category {
222                    ErrorCategory::Internal => Code::Internal,
223                    ErrorCategory::Aborted => Code::Aborted,
224                    ErrorCategory::InvalidTransaction => Code::InvalidArgument,
225                    ErrorCategory::LockConflict => Code::FailedPrecondition,
226                    ErrorCategory::ValidatorOverloaded => Code::ResourceExhausted,
227                    ErrorCategory::Unavailable => Code::Unavailable,
228                },
229                details,
230            ),
231        }
232    }
233}
234
235impl From<crate::proto::google::rpc::bad_request::FieldViolation> for RpcError {
236    fn from(value: crate::proto::google::rpc::bad_request::FieldViolation) -> Self {
237        BadRequest::from(value).into()
238    }
239}
240
241impl From<BadRequest> for RpcError {
242    fn from(value: BadRequest) -> Self {
243        let message = value
244            .field_violations
245            .first()
246            .map(|violation| violation.description.clone());
247        let details = ErrorDetails::new().with_bad_request(value);
248
249        RpcError {
250            code: Code::InvalidArgument,
251            message,
252            details: Some(Box::new(details)),
253        }
254    }
255}
256
257#[derive(Clone, Debug, Default)]
258pub struct ErrorDetails {
259    error_info: Option<ErrorInfo>,
260    bad_request: Option<BadRequest>,
261    retry_info: Option<RetryInfo>,
262}
263
264impl ErrorDetails {
265    pub fn new() -> Self {
266        Self::default()
267    }
268
269    pub fn error_info(&self) -> Option<&ErrorInfo> {
270        self.error_info.as_ref()
271    }
272
273    pub fn bad_request(&self) -> Option<&BadRequest> {
274        self.bad_request.as_ref()
275    }
276
277    pub fn retry_info(&self) -> Option<&RetryInfo> {
278        self.retry_info.as_ref()
279    }
280
281    pub fn details(&self) -> &[prost_types::Any] {
282        &[]
283    }
284
285    pub fn with_bad_request(mut self, bad_request: BadRequest) -> Self {
286        self.bad_request = Some(bad_request);
287        self
288    }
289
290    #[allow(clippy::boxed_local)]
291    fn into_status_details(self: Box<Self>) -> Vec<prost_types::Any> {
292        let mut details = Vec::new();
293
294        if let Some(error_info) = &self.error_info {
295            details.push(
296                prost_types::Any::from_msg(error_info).expect("Message encoding cannot fail"),
297            );
298        }
299
300        if let Some(bad_request) = &self.bad_request {
301            details.push(
302                prost_types::Any::from_msg(bad_request).expect("Message encoding cannot fail"),
303            );
304        }
305
306        if let Some(retry_info) = &self.retry_info {
307            details.push(
308                prost_types::Any::from_msg(retry_info).expect("Message encoding cannot fail"),
309            );
310        }
311        details
312    }
313}
314
315#[derive(Debug)]
316pub struct ObjectNotFoundError {
317    object_id: sui_sdk_types::Address,
318    version: Option<sui_sdk_types::Version>,
319}
320
321impl ObjectNotFoundError {
322    pub fn new(object_id: sui_sdk_types::Address) -> Self {
323        Self {
324            object_id,
325            version: None,
326        }
327    }
328
329    pub fn new_with_version(
330        object_id: sui_sdk_types::Address,
331        version: sui_sdk_types::Version,
332    ) -> Self {
333        Self {
334            object_id,
335            version: Some(version),
336        }
337    }
338}
339
340impl std::fmt::Display for ObjectNotFoundError {
341    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342        write!(f, "Object {}", self.object_id)?;
343
344        if let Some(version) = self.version {
345            write!(f, " with version {version}")?;
346        }
347
348        write!(f, " not found")
349    }
350}
351
352impl std::error::Error for ObjectNotFoundError {}
353
354impl From<ObjectNotFoundError> for crate::RpcError {
355    fn from(value: ObjectNotFoundError) -> Self {
356        Self::new(tonic::Code::NotFound, value.to_string())
357    }
358}
359
360#[derive(Debug)]
361pub struct CheckpointNotFoundError {
362    sequence_number: Option<u64>,
363    digest: Option<sui_sdk_types::Digest>,
364}
365
366impl CheckpointNotFoundError {
367    pub fn sequence_number(sequence_number: u64) -> Self {
368        Self {
369            sequence_number: Some(sequence_number),
370            digest: None,
371        }
372    }
373
374    pub fn digest(digest: sui_sdk_types::Digest) -> Self {
375        Self {
376            sequence_number: None,
377            digest: Some(digest),
378        }
379    }
380}
381
382impl std::fmt::Display for CheckpointNotFoundError {
383    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384        write!(f, "Checkpoint ")?;
385
386        if let Some(s) = self.sequence_number {
387            write!(f, "{s} ")?;
388        }
389
390        if let Some(d) = &self.digest {
391            write!(f, "{d} ")?;
392        }
393
394        write!(f, "not found")
395    }
396}
397
398impl std::error::Error for CheckpointNotFoundError {}
399
400impl From<CheckpointNotFoundError> for crate::RpcError {
401    fn from(value: CheckpointNotFoundError) -> Self {
402        Self::new(tonic::Code::NotFound, value.to_string())
403    }
404}