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