1use 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#[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 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 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 .sorted_by(|(_, a, _), (_, b, _)| b.cmp(a))
191 .filter_map(|(err, _, _)| {
192 match err.as_inner() {
193 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 RpcError::new(Code::Unavailable, "system is overloaded")
235 }
236 TransactionFailed { category, details } => RpcError::new(
237 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#[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}