Skip to main content

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};
8use crate::proto::sui::rpc::v2::ExecutionError;
9pub use sui_rpc::proto::sui::rpc::v2::ErrorReason;
10
11pub type Result<T, E = RpcError> = std::result::Result<T, E>;
12
13/// An error encountered while serving an RPC request.
14///
15/// General error type used by top-level RPC service methods. The main purpose of this error type
16/// is to provide a convenient type for converting between internal errors and a response that
17/// needs to be sent to a calling client.
18#[derive(Debug)]
19pub struct RpcError {
20    code: Code,
21    message: Option<String>,
22    details: Option<Box<ErrorDetails>>,
23}
24
25impl RpcError {
26    pub fn new<T: Into<String>>(code: Code, message: T) -> Self {
27        Self {
28            code,
29            message: Some(message.into()),
30            details: None,
31        }
32    }
33
34    pub fn not_found() -> Self {
35        Self {
36            code: Code::NotFound,
37            message: None,
38            details: None,
39        }
40    }
41
42    pub fn with_details(mut self, details: ErrorDetails) -> Self {
43        self.details = Some(Box::new(details));
44        self
45    }
46
47    pub fn into_status_proto(self) -> crate::proto::google::rpc::Status {
48        crate::proto::google::rpc::Status {
49            code: self.code.into(),
50            message: self.message.unwrap_or_default(),
51            details: self
52                .details
53                .map(ErrorDetails::into_status_details)
54                .unwrap_or_default(),
55        }
56    }
57}
58
59impl std::fmt::Display for RpcError {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        match &self.message {
62            Some(m) => write!(f, "{}: {}", self.code, m),
63            None => write!(f, "{}", self.code),
64        }
65    }
66}
67
68impl std::error::Error for RpcError {}
69
70impl From<RpcError> for tonic::Status {
71    fn from(value: RpcError) -> Self {
72        use prost::Message;
73
74        let code = value.code;
75        let status = value.into_status_proto();
76        let details = status.encode_to_vec().into();
77        let message = status.message;
78
79        tonic::Status::with_details(code, message, details)
80    }
81}
82
83impl From<sui_types::storage::error::Error> for RpcError {
84    fn from(value: sui_types::storage::error::Error) -> Self {
85        use sui_types::storage::error::Kind;
86
87        let code = match value.kind() {
88            Kind::Missing => Code::NotFound,
89            _ => Code::Internal,
90        };
91
92        Self {
93            code,
94            message: Some(value.to_string()),
95            details: None,
96        }
97    }
98}
99
100impl From<anyhow::Error> for RpcError {
101    fn from(value: anyhow::Error) -> Self {
102        Self {
103            code: Code::Internal,
104            message: Some(value.to_string()),
105            details: None,
106        }
107    }
108}
109
110impl From<sui_types::sui_sdk_types_conversions::SdkTypeConversionError> for RpcError {
111    fn from(value: sui_types::sui_sdk_types_conversions::SdkTypeConversionError) -> Self {
112        Self {
113            code: Code::Internal,
114            message: Some(value.to_string()),
115            details: None,
116        }
117    }
118}
119
120impl From<bcs::Error> for RpcError {
121    fn from(value: bcs::Error) -> Self {
122        Self {
123            code: Code::Internal,
124            message: Some(value.to_string()),
125            details: None,
126        }
127    }
128}
129
130impl From<sui_types::transaction_driver_types::TransactionSubmissionError> for RpcError {
131    fn from(error: sui_types::transaction_driver_types::TransactionSubmissionError) -> Self {
132        use itertools::Itertools;
133        use sui_types::error::SuiErrorKind;
134        use sui_types::transaction_driver_types::TransactionSubmissionError::*;
135
136        match error {
137            InvalidUserSignature(err) => {
138                let message = {
139                    let err = match err.as_inner() {
140                        SuiErrorKind::UserInputError { error } => error.to_string(),
141                        _ => err.to_string(),
142                    };
143                    format!("Invalid user signature: {err}")
144                };
145
146                RpcError::new(Code::InvalidArgument, message)
147            }
148            TransactionDriverInternalError(err) => RpcError::new(Code::Internal, err.to_string()),
149            ObjectsDoubleUsed { conflicting_txes } => {
150                let new_map = conflicting_txes
151                    .into_iter()
152                    .map(|(digest, (pairs, _))| {
153                        (
154                            digest,
155                            pairs.into_iter().map(|(_, obj_ref)| obj_ref).collect(),
156                        )
157                    })
158                    .collect::<std::collections::BTreeMap<_, Vec<_>>>();
159
160                let message = format!(
161                    "Failed to sign transaction by a quorum of validators because of locked objects. Conflicting Transactions:\n{new_map:#?}",
162                );
163
164                RpcError::new(Code::FailedPrecondition, message)
165            }
166            TimeoutBeforeFinality | FailedWithTransientErrorAfterMaximumAttempts { .. } => {
167                // TODO add a Retry-After header
168                RpcError::new(
169                    Code::Unavailable,
170                    "timed-out before finality could be reached",
171                )
172            }
173            TimeoutBeforeFinalityWithErrors {
174                last_error,
175                attempts,
176                timeout,
177            } => {
178                // TODO add a Retry-After header
179                RpcError::new(
180                    Code::Unavailable,
181                    format!(
182                        "Transaction timed out before finality could be reached. Attempts: {attempts} & timeout: {timeout:?}. Last error: {last_error}"
183                    ),
184                )
185            }
186            NonRecoverableTransactionError { errors } => {
187                let new_errors: Vec<String> = errors
188                    .into_iter()
189                    // sort by total stake, descending, so users see the most prominent one first
190                    .sorted_by(|(_, a, _), (_, b, _)| b.cmp(a))
191                    .filter_map(|(err, _, _)| {
192                        match err.as_inner() {
193                            // Special handling of UserInputError:
194                            // ObjectNotFound and DependentPackageNotFound are considered
195                            // retryable errors but they have different treatment
196                            // in AuthorityAggregator.
197                            // The optimal fix would be to examine if the total stake
198                            // of ObjectNotFound/DependentPackageNotFound exceeds the
199                            // quorum threshold, but it takes a Committee here.
200                            // So, we take an easier route and consider them non-retryable
201                            // at all. Combining this with the sorting above, clients will
202                            // see the dominant error first.
203                            SuiErrorKind::UserInputError { error } => Some(error.to_string()),
204                            _ => {
205                                if err.is_retryable().0 {
206                                    None
207                                } else {
208                                    Some(err.to_string())
209                                }
210                            }
211                        }
212                    })
213                    .collect();
214
215                assert!(
216                    !new_errors.is_empty(),
217                    "NonRecoverableTransactionError should have at least one non-retryable error"
218                );
219
220                let error_list = new_errors.join(", ");
221                let error_msg = format!(
222                    "Transaction execution failed due to issues with transaction inputs, please review the errors and try again: {}.",
223                    error_list
224                );
225
226                RpcError::new(Code::InvalidArgument, error_msg)
227            }
228            TxAlreadyFinalizedWithDifferentUserSignatures => RpcError::new(
229                Code::Aborted,
230                "The transaction is already finalized but with different user signatures",
231            ),
232            SystemOverload { .. } | SystemOverloadRetryAfter { .. } => {
233                // TODO add a Retry-After header
234                RpcError::new(Code::Unavailable, "system is overloaded")
235            }
236            TransactionFailed { category, details } => RpcError::new(
237                // TODO(fastpath): add a Retry-After header.
238                match category {
239                    ErrorCategory::Internal => Code::Internal,
240                    ErrorCategory::Aborted => Code::Aborted,
241                    ErrorCategory::InvalidTransaction => Code::InvalidArgument,
242                    ErrorCategory::LockConflict => Code::FailedPrecondition,
243                    ErrorCategory::ValidatorOverloaded => Code::ResourceExhausted,
244                    ErrorCategory::Unavailable => Code::Unavailable,
245                },
246                details,
247            ),
248        }
249    }
250}
251
252impl From<crate::proto::google::rpc::bad_request::FieldViolation> for RpcError {
253    fn from(value: crate::proto::google::rpc::bad_request::FieldViolation) -> Self {
254        BadRequest::from(value).into()
255    }
256}
257
258impl From<BadRequest> for RpcError {
259    fn from(value: BadRequest) -> Self {
260        let message = value
261            .field_violations
262            .first()
263            .map(|violation| violation.description.clone());
264        let details = ErrorDetails::new().with_bad_request(value);
265
266        RpcError {
267            code: Code::InvalidArgument,
268            message,
269            details: Some(Box::new(details)),
270        }
271    }
272}
273
274#[derive(Clone, Debug, Default)]
275pub struct ErrorDetails {
276    error_info: Option<ErrorInfo>,
277    bad_request: Option<BadRequest>,
278    retry_info: Option<RetryInfo>,
279    execution_error: Option<ExecutionError>,
280}
281
282impl ErrorDetails {
283    pub fn new() -> Self {
284        Self::default()
285    }
286
287    pub fn error_info(&self) -> Option<&ErrorInfo> {
288        self.error_info.as_ref()
289    }
290
291    pub fn bad_request(&self) -> Option<&BadRequest> {
292        self.bad_request.as_ref()
293    }
294
295    pub fn retry_info(&self) -> Option<&RetryInfo> {
296        self.retry_info.as_ref()
297    }
298
299    pub fn execution_error(&self) -> Option<&ExecutionError> {
300        self.execution_error.as_ref()
301    }
302
303    pub fn details(&self) -> &[prost_types::Any] {
304        &[]
305    }
306
307    pub fn with_error_info(mut self, error_info: ErrorInfo) -> Self {
308        self.error_info = Some(error_info);
309        self
310    }
311
312    pub fn with_bad_request(mut self, bad_request: BadRequest) -> Self {
313        self.bad_request = Some(bad_request);
314        self
315    }
316
317    pub fn with_execution_error(mut self, execution_error: ExecutionError) -> Self {
318        self.execution_error = Some(execution_error);
319        self
320    }
321
322    #[allow(clippy::boxed_local)]
323    fn into_status_details(self: Box<Self>) -> Vec<prost_types::Any> {
324        let mut details = Vec::new();
325
326        if let Some(error_info) = &self.error_info {
327            details.push(
328                prost_types::Any::from_msg(error_info).expect("Message encoding cannot fail"),
329            );
330        }
331
332        if let Some(bad_request) = &self.bad_request {
333            details.push(
334                prost_types::Any::from_msg(bad_request).expect("Message encoding cannot fail"),
335            );
336        }
337
338        if let Some(retry_info) = &self.retry_info {
339            details.push(
340                prost_types::Any::from_msg(retry_info).expect("Message encoding cannot fail"),
341            );
342        }
343
344        if let Some(execution_error) = &self.execution_error {
345            details.push(
346                prost_types::Any::from_msg(execution_error).expect("Message encoding cannot fail"),
347            );
348        }
349        details
350    }
351}
352
353#[derive(Debug)]
354pub struct ObjectNotFoundError {
355    object_id: sui_sdk_types::Address,
356    version: Option<sui_sdk_types::Version>,
357}
358
359impl ObjectNotFoundError {
360    pub fn new(object_id: sui_sdk_types::Address) -> Self {
361        Self {
362            object_id,
363            version: None,
364        }
365    }
366
367    pub fn new_with_version(
368        object_id: sui_sdk_types::Address,
369        version: sui_sdk_types::Version,
370    ) -> Self {
371        Self {
372            object_id,
373            version: Some(version),
374        }
375    }
376}
377
378impl std::fmt::Display for ObjectNotFoundError {
379    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380        write!(f, "Object {}", self.object_id)?;
381
382        if let Some(version) = self.version {
383            write!(f, " with version {version}")?;
384        }
385
386        write!(f, " not found")
387    }
388}
389
390impl std::error::Error for ObjectNotFoundError {}
391
392impl From<ObjectNotFoundError> for crate::RpcError {
393    fn from(value: ObjectNotFoundError) -> Self {
394        Self::new(tonic::Code::NotFound, value.to_string())
395    }
396}
397
398#[derive(Debug)]
399pub struct CheckpointNotFoundError {
400    sequence_number: Option<u64>,
401    digest: Option<sui_sdk_types::Digest>,
402}
403
404impl CheckpointNotFoundError {
405    pub fn sequence_number(sequence_number: u64) -> Self {
406        Self {
407            sequence_number: Some(sequence_number),
408            digest: None,
409        }
410    }
411
412    pub fn digest(digest: sui_sdk_types::Digest) -> Self {
413        Self {
414            sequence_number: None,
415            digest: Some(digest),
416        }
417    }
418}
419
420impl std::fmt::Display for CheckpointNotFoundError {
421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422        write!(f, "Checkpoint ")?;
423
424        if let Some(s) = self.sequence_number {
425            write!(f, "{s} ")?;
426        }
427
428        if let Some(d) = &self.digest {
429            write!(f, "{d} ")?;
430        }
431
432        write!(f, "not found")
433    }
434}
435
436impl std::error::Error for CheckpointNotFoundError {}
437
438impl From<CheckpointNotFoundError> for crate::RpcError {
439    fn from(value: CheckpointNotFoundError) -> Self {
440        Self::new(tonic::Code::NotFound, value.to_string())
441    }
442}
443
444#[derive(Debug)]
445pub struct EpochNotFoundError {
446    epoch: sui_sdk_types::EpochId,
447}
448
449impl EpochNotFoundError {
450    pub fn new(epoch: sui_sdk_types::EpochId) -> Self {
451        Self { epoch }
452    }
453}
454
455impl std::fmt::Display for EpochNotFoundError {
456    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457        write!(f, "Epoch {} not found", self.epoch)
458    }
459}
460
461impl std::error::Error for EpochNotFoundError {}
462
463impl From<EpochNotFoundError> for crate::RpcError {
464    fn from(value: EpochNotFoundError) -> Self {
465        Self::new(tonic::Code::NotFound, value.to_string())
466    }
467}
468
469/// Gas selection could not find enough SUI to cover the transaction's gas budget.
470#[derive(Debug)]
471pub struct InsufficientGasBalanceError {
472    owner: sui_types::base_types::SuiAddress,
473    required_budget: u64,
474    coin_balance: u64,
475    address_balance: u64,
476}
477
478impl InsufficientGasBalanceError {
479    pub fn new(
480        owner: sui_types::base_types::SuiAddress,
481        required_budget: u64,
482        coin_balance: u64,
483        address_balance: u64,
484    ) -> Self {
485        Self {
486            owner,
487            required_budget,
488            coin_balance,
489            address_balance,
490        }
491    }
492}
493
494impl std::fmt::Display for InsufficientGasBalanceError {
495    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
496        write!(
497            f,
498            "Unable to perform gas selection due to insufficient SUI \
499            balance (in address balance or coins) for account {} \
500            to satisfy required budget {}.",
501            self.owner, self.required_budget,
502        )
503    }
504}
505
506impl std::error::Error for InsufficientGasBalanceError {}
507
508impl From<InsufficientGasBalanceError> for crate::RpcError {
509    fn from(value: InsufficientGasBalanceError) -> Self {
510        let metadata = [
511            ("owner", value.owner.to_string()),
512            ("requiredBudget", value.required_budget.to_string()),
513            ("coinBalance", value.coin_balance.to_string()),
514            ("addressBalance", value.address_balance.to_string()),
515        ]
516        .into_iter()
517        .map(|(key, metadata_value)| (key.to_owned(), metadata_value))
518        .collect();
519        let error_info = ErrorInfo::default()
520            .with_reason(ErrorReason::InsufficientGasBalance.as_str_name())
521            .with_domain(ErrorReason::DOMAIN)
522            .with_metadata(metadata);
523        let details = ErrorDetails::new().with_error_info(error_info);
524
525        Self {
526            code: Code::InvalidArgument,
527            message: Some(value.to_string()),
528            details: Some(Box::new(details)),
529        }
530    }
531}