Skip to main content

iota_json_rpc/
error.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::collections::BTreeMap;
6
7use fastcrypto::error::FastCryptoError;
8use hyper::header::InvalidHeaderValue;
9use iota_json_rpc_api::{
10    TRANSACTION_EXECUTION_CLIENT_ERROR_CODE, TRANSACTION_NOT_FOUND_ERROR_CODE,
11    TRANSIENT_ERROR_CODE, error_object_from_rpc,
12};
13use iota_json_rpc_types::IotaObjectResponseError;
14use iota_names::error::IotaNamesError;
15use iota_types::{
16    error::{IotaError, UserInputError},
17    quorum_driver_types::QuorumDriverError,
18};
19use jsonrpsee::{
20    core::{ClientError as RpcError, RegisterMethodError},
21    types::{
22        ErrorObject, ErrorObjectOwned,
23        error::{CALL_EXECUTION_FAILED_CODE, ErrorCode, INTERNAL_ERROR_CODE},
24    },
25};
26use thiserror::Error;
27use tokio::task::JoinError;
28
29use crate::authority_state::StateReadError;
30
31pub type RpcInterimResult<T = ()> = Result<T, Error>;
32
33#[derive(Debug, Error)]
34#[non_exhaustive]
35pub enum Error {
36    #[error(transparent)]
37    Iota(IotaError),
38
39    #[error(transparent)]
40    Internal(#[from] anyhow::Error),
41
42    #[error("Deserialization error: {0}")]
43    Bcs(#[from] bcs::Error),
44    #[error("Unexpected error: {0}")]
45    Unexpected(String),
46
47    #[error(transparent)]
48    RPCServer(#[from] RpcError),
49    #[error(transparent)]
50    RPCRegisterMethod(#[from] RegisterMethodError),
51
52    #[error(transparent)]
53    InvalidHeaderValue(#[from] InvalidHeaderValue),
54
55    #[error(transparent)]
56    UserInput(#[from] UserInputError),
57
58    #[error(transparent)]
59    Encoding(#[from] eyre::Report),
60
61    #[error(transparent)]
62    TokioJoin(#[from] JoinError),
63
64    #[error(transparent)]
65    QuorumDriver(#[from] QuorumDriverError),
66
67    #[error(transparent)]
68    FastCrypto(#[from] FastCryptoError),
69
70    #[error(transparent)]
71    IotaObjectResponse(#[from] IotaObjectResponseError),
72
73    #[error(transparent)]
74    IotaRpcInput(#[from] IotaRpcInputError),
75
76    // TODO(wlmyng): convert StateReadError::Internal message to generic internal error message.
77    #[error(transparent)]
78    StateRead(#[from] StateReadError),
79
80    #[error("Unsupported Feature: {0}")]
81    UnsupportedFeature(String),
82
83    #[error(transparent)]
84    IotaNames(#[from] IotaNamesError),
85}
86
87impl From<IotaError> for Error {
88    fn from(e: IotaError) -> Self {
89        match e {
90            IotaError::UserInput { error } => Self::UserInput(error),
91            IotaError::UnsupportedFeature { error } => Self::UnsupportedFeature(error),
92            IotaError::IndexStoreNotAvailable => Self::UnsupportedFeature(
93                "Required indexes are not available on this node".to_string(),
94            ),
95            other => Self::Iota(other),
96        }
97    }
98}
99
100impl From<Error> for RpcError {
101    /// `InvalidParams`/`INVALID_PARAMS_CODE` for client errors.
102    fn from(e: Error) -> RpcError {
103        match e {
104            Error::UserInput(_) | Error::UnsupportedFeature(_) => RpcError::Call(
105                ErrorObject::owned::<()>(ErrorCode::InvalidRequest.code(), e.to_string(), None),
106            ),
107            Error::IotaObjectResponse(err) => match err {
108                IotaObjectResponseError::NotExists { .. }
109                | IotaObjectResponseError::DynamicFieldNotFound { .. }
110                | IotaObjectResponseError::Deleted { .. }
111                | IotaObjectResponseError::Display { .. } => {
112                    RpcError::Call(ErrorObject::owned::<()>(
113                        ErrorCode::InvalidParams.code(),
114                        err.to_string(),
115                        None,
116                    ))
117                }
118                _ => RpcError::Call(ErrorObject::owned::<()>(
119                    CALL_EXECUTION_FAILED_CODE,
120                    err.to_string(),
121                    None,
122                )),
123            },
124            Error::IotaRpcInput(err) => RpcError::Call(ErrorObject::owned::<()>(
125                ErrorCode::InvalidParams.code(),
126                err.to_string(),
127                None,
128            )),
129            Error::Iota(iota_error) => match iota_error {
130                IotaError::TransactionNotFound { .. } => RpcError::Call(ErrorObject::owned::<()>(
131                    TRANSACTION_NOT_FOUND_ERROR_CODE,
132                    iota_error.to_string(),
133                    None,
134                )),
135                IotaError::TransactionsNotFound { .. }
136                | IotaError::TransactionEventsNotFound { .. } => {
137                    RpcError::Call(ErrorObject::owned::<()>(
138                        ErrorCode::InvalidParams.code(),
139                        iota_error.to_string(),
140                        None,
141                    ))
142                }
143                _ => RpcError::Call(ErrorObject::owned::<()>(
144                    CALL_EXECUTION_FAILED_CODE,
145                    iota_error.to_string(),
146                    None,
147                )),
148            },
149            Error::StateRead(err) => match err {
150                StateReadError::Client(_) => RpcError::Call(ErrorObject::owned::<()>(
151                    ErrorCode::InvalidParams.code(),
152                    err.to_string(),
153                    None,
154                )),
155                _ => {
156                    let error_object = ErrorObject::owned::<()>(
157                        jsonrpsee::types::error::INTERNAL_ERROR_CODE,
158                        err.to_string(),
159                        None,
160                    );
161                    RpcError::Call(error_object)
162                }
163            },
164            Error::QuorumDriver(err) => {
165                let error_msg = err.to_error_message();
166                // Machine-readable discriminant: the codes below are shared
167                // between variants, so clients disambiguate via
168                // `data.reason` instead of parsing the message. Overload
169                // hints are added as `data.retry_after_secs`.
170                let mut data = serde_json::json!({ "reason": err.reason() });
171                if let QuorumDriverError::SystemOverloadRetryAfter {
172                    retry_after_secs, ..
173                } = &err
174                {
175                    data["retry_after_secs"] = (*retry_after_secs).into();
176                }
177
178                match err {
179                    QuorumDriverError::InvalidUserSignature { .. }
180                    | QuorumDriverError::InvalidTransaction { .. }
181                    | QuorumDriverError::RejectedByValidators { .. }
182                    | QuorumDriverError::TxAlreadyFinalizedWithDifferentUserSignatures
183                    | QuorumDriverError::NonRecoverableTransactionError { .. } => {
184                        let error_object = ErrorObject::owned(
185                            TRANSACTION_EXECUTION_CLIENT_ERROR_CODE,
186                            error_msg,
187                            Some(data),
188                        );
189                        RpcError::Call(error_object)
190                    }
191                    QuorumDriverError::ObjectsDoubleUsed { conflicting_txes } => {
192                        let new_map = conflicting_txes
193                            .into_iter()
194                            .map(|(digest, (pairs, _))| {
195                                (
196                                    digest,
197                                    pairs.into_iter().map(|(_, obj_ref)| obj_ref).collect(),
198                                )
199                            })
200                            .collect::<BTreeMap<_, Vec<_>>>();
201
202                        let error_object = ErrorObject::owned(
203                            TRANSACTION_EXECUTION_CLIENT_ERROR_CODE,
204                            error_msg,
205                            Some(new_map),
206                        );
207                        RpcError::Call(error_object)
208                    }
209                    QuorumDriverError::TimeoutBeforeFinality
210                    | QuorumDriverError::FailedWithTransientErrorAfterMaximumAttempts { .. }
211                    | QuorumDriverError::SystemOverload { .. }
212                    | QuorumDriverError::SystemOverloadRetryAfter { .. } => {
213                        let error_object =
214                            ErrorObject::owned(TRANSIENT_ERROR_CODE, error_msg, Some(data));
215                        RpcError::Call(error_object)
216                    }
217                    QuorumDriverError::QuorumDriverInternal(_) => {
218                        let error_object =
219                            ErrorObject::owned::<()>(INTERNAL_ERROR_CODE, error_msg, None);
220                        RpcError::Call(error_object)
221                    }
222                }
223            }
224            _ => RpcError::Call(ErrorObject::owned::<()>(
225                CALL_EXECUTION_FAILED_CODE,
226                e.to_string(),
227                None,
228            )),
229        }
230    }
231}
232
233impl From<Error> for ErrorObjectOwned {
234    fn from(value: Error) -> Self {
235        error_object_from_rpc(value.into())
236    }
237}
238
239#[derive(Debug, Error)]
240pub enum IotaRpcInputError {
241    #[error("Input contains duplicates")]
242    ContainsDuplicates,
243
244    #[error("Input exceeds limit of {0}")]
245    SizeLimitExceeded(String),
246
247    #[error("{0}")]
248    GenericNotFound(String),
249
250    #[error("{0}")]
251    GenericInvalid(String),
252
253    #[error("Unsupported protocol version requested. Min supported: {0}, max supported: {1}")]
254    ProtocolVersionUnsupported(u64, u64),
255
256    #[error("{0}")]
257    CannotParseIotaStructTag(String),
258
259    #[error(transparent)]
260    Base64(#[from] eyre::Report),
261
262    #[error("Deserialization error: {0}")]
263    Bcs(#[from] bcs::Error),
264
265    #[error(transparent)]
266    FastCrypto(#[from] FastCryptoError),
267
268    #[error(transparent)]
269    Anyhow(#[from] anyhow::Error),
270
271    #[error(transparent)]
272    UserInput(#[from] UserInputError),
273}
274
275impl From<IotaRpcInputError> for RpcError {
276    fn from(e: IotaRpcInputError) -> Self {
277        RpcError::Call(ErrorObject::owned::<()>(
278            ErrorCode::InvalidParams.code(),
279            e.to_string(),
280            None,
281        ))
282    }
283}
284
285impl From<IotaRpcInputError> for ErrorObjectOwned {
286    fn from(value: IotaRpcInputError) -> Self {
287        error_object_from_rpc(value.into())
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use expect_test::expect;
294    use iota_sdk_types::{ObjectDigest, ObjectId, ObjectReference, TransactionDigest, Version};
295    use iota_types::{
296        base_types::AuthorityName,
297        committee::StakeUnit,
298        crypto::{AuthorityPublicKey, AuthorityPublicKeyBytes},
299    };
300
301    use super::*;
302
303    fn test_object_ref() -> ObjectReference {
304        ObjectReference::new(
305            ObjectId::ZERO,
306            Version::from_u64(0),
307            ObjectDigest::new([0; 32]),
308        )
309    }
310
311    #[test]
312    fn transaction_not_found_uses_dedicated_error_code() {
313        let error = Error::Iota(IotaError::TransactionNotFound {
314            digest: TransactionDigest::from([1; 32]),
315        });
316
317        let rpc_error: RpcError = error.into();
318        let error_object = error_object_from_rpc(rpc_error);
319
320        assert_eq!(error_object.code(), TRANSACTION_NOT_FOUND_ERROR_CODE);
321    }
322
323    mod match_quorum_driver_error_tests {
324        use super::*;
325
326        #[test]
327        fn test_invalid_user_signature() {
328            let quorum_driver_error =
329                QuorumDriverError::InvalidUserSignature(IotaError::InvalidSignature {
330                    error: "Test inner invalid signature".to_string(),
331                });
332
333            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
334
335            let error_object = error_object_from_rpc(rpc_error);
336            let expected_code = expect!["-32002"];
337            expected_code.assert_eq(&error_object.code().to_string());
338            let expected_message = expect![
339                "Invalid user signature: Signature is not valid: Test inner invalid signature"
340            ];
341            expected_message.assert_eq(error_object.message());
342            let expected_data = expect![[r#"{"reason":"invalid_user_signature"}"#]];
343            expected_data.assert_eq(&error_object.data().unwrap().to_string());
344        }
345
346        #[test]
347        fn test_invalid_transaction_vs_rejected_by_validators() {
348            // Same rendered code and message; only `data.reason` tells a
349            // locally proven invalid transaction apart from one rejected by
350            // the polled validators.
351            let inner = || IotaError::Unknown("Test rejection".to_string());
352
353            let error_object = error_object_from_rpc(
354                Error::QuorumDriver(QuorumDriverError::InvalidTransaction(inner())).into(),
355            );
356            let expected_code = expect!["-32002"];
357            expected_code.assert_eq(&error_object.code().to_string());
358            let expected_message = expect!["Invalid transaction: unknown error: Test rejection"];
359            expected_message.assert_eq(error_object.message());
360            let expected_data = expect![[r#"{"reason":"invalid_transaction"}"#]];
361            expected_data.assert_eq(&error_object.data().unwrap().to_string());
362
363            let error_object = error_object_from_rpc(
364                Error::QuorumDriver(QuorumDriverError::RejectedByValidators(inner())).into(),
365            );
366            let expected_code = expect!["-32002"];
367            expected_code.assert_eq(&error_object.code().to_string());
368            let expected_message = expect!["Invalid transaction: unknown error: Test rejection"];
369            expected_message.assert_eq(error_object.message());
370            let expected_data = expect![[r#"{"reason":"rejected_by_validators"}"#]];
371            expected_data.assert_eq(&error_object.data().unwrap().to_string());
372        }
373
374        #[test]
375        fn test_timeout_before_finality() {
376            let quorum_driver_error = QuorumDriverError::TimeoutBeforeFinality;
377
378            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
379
380            let error_object = error_object_from_rpc(rpc_error);
381            let expected_code = expect!["-32050"];
382            expected_code.assert_eq(&error_object.code().to_string());
383            let expected_message = expect!["Transaction timed out before reaching finality"];
384            expected_message.assert_eq(error_object.message());
385            let expected_data = expect![[r#"{"reason":"timeout_before_finality"}"#]];
386            expected_data.assert_eq(&error_object.data().unwrap().to_string());
387        }
388
389        #[test]
390        fn test_failed_with_transient_error_after_maximum_attempts() {
391            let quorum_driver_error =
392                QuorumDriverError::FailedWithTransientErrorAfterMaximumAttempts {
393                    total_attempts: 10,
394                };
395
396            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
397
398            let error_object = error_object_from_rpc(rpc_error);
399            let expected_code = expect!["-32050"];
400            expected_code.assert_eq(&error_object.code().to_string());
401            let expected_message = expect![
402                "Transaction failed to reach finality with transient error after 10 attempts."
403            ];
404            expected_message.assert_eq(error_object.message());
405            let expected_data =
406                expect![[r#"{"reason":"failed_with_transient_error_after_maximum_attempts"}"#]];
407            expected_data.assert_eq(&error_object.data().unwrap().to_string());
408        }
409
410        #[test]
411        fn test_objects_double_used() {
412            use iota_types::crypto::VerifyingKey;
413            let mut conflicting_txes: BTreeMap<
414                TransactionDigest,
415                (Vec<(AuthorityName, ObjectReference)>, StakeUnit),
416            > = BTreeMap::new();
417            let tx_digest = TransactionDigest::from([1; 32]);
418            let object_ref = test_object_ref();
419            // 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi has enough stake to escape
420            // equivocation
421            let stake_unit: StakeUnit = 8000;
422            let authority_name = AuthorityPublicKeyBytes([0; AuthorityPublicKey::LENGTH]);
423            conflicting_txes.insert(tx_digest, (vec![(authority_name, object_ref)], stake_unit));
424
425            // 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR stake below quorum threshold
426            let tx_digest = TransactionDigest::from([2; 32]);
427            let stake_unit: StakeUnit = 500;
428            let authority_name = AuthorityPublicKeyBytes([1; AuthorityPublicKey::LENGTH]);
429            conflicting_txes.insert(tx_digest, (vec![(authority_name, object_ref)], stake_unit));
430
431            let quorum_driver_error = QuorumDriverError::ObjectsDoubleUsed { conflicting_txes };
432
433            let error_object: ErrorObjectOwned = Error::QuorumDriver(quorum_driver_error).into();
434
435            let expected_code = expect!["-32002"];
436            expected_code.assert_eq(&error_object.code().to_string());
437            println!("error_object.message() {}", error_object.message());
438            let expected_message = expect![[r#"
439                Failed to sign transaction by a quorum of validators because one or more of its objects is reserved for another transaction. Other transactions locking these objects:
440                - 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi (stake 80.0)
441                - 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR (stake 5.0)"#]];
442            expected_message.assert_eq(error_object.message());
443            let expected_data = expect![[
444                r#"{"4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi":[{"object_id":"0x0000000000000000000000000000000000000000000000000000000000000000","version":"0","digest":"11111111111111111111111111111111"}],"8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR":[{"object_id":"0x0000000000000000000000000000000000000000000000000000000000000000","version":"0","digest":"11111111111111111111111111111111"}]}"#
445            ]];
446            let actual_data = error_object.data().unwrap().to_string();
447            expected_data.assert_eq(&actual_data);
448        }
449
450        #[test]
451        fn test_objects_double_used_equivocated() {
452            use iota_types::crypto::VerifyingKey;
453            let mut conflicting_txes: BTreeMap<
454                TransactionDigest,
455                (Vec<(AuthorityName, ObjectReference)>, StakeUnit),
456            > = BTreeMap::new();
457            let tx_digest = TransactionDigest::from([1; 32]);
458            let object_ref = test_object_ref();
459
460            // 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi has lower stake at 10
461            let stake_unit: StakeUnit = 4000;
462            let authority_name = AuthorityPublicKeyBytes([0; AuthorityPublicKey::LENGTH]);
463            conflicting_txes.insert(tx_digest, (vec![(authority_name, object_ref)], stake_unit));
464
465            // 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR is a higher stake and should be
466            // first in the list
467            let tx_digest = TransactionDigest::from([2; 32]);
468            let stake_unit: StakeUnit = 5000;
469            let authority_name = AuthorityPublicKeyBytes([1; AuthorityPublicKey::LENGTH]);
470            conflicting_txes.insert(tx_digest, (vec![(authority_name, object_ref)], stake_unit));
471
472            let quorum_driver_error = QuorumDriverError::ObjectsDoubleUsed { conflicting_txes };
473
474            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
475
476            let error_object = error_object_from_rpc(rpc_error);
477            let expected_code = expect!["-32002"];
478            expected_code.assert_eq(&error_object.code().to_string());
479            let expected_message = expect![[r#"
480                Failed to sign transaction by a quorum of validators because one or more of its objects is equivocated until the next epoch. Other transactions locking these objects:
481                - 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR (stake 50.0)
482                - 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi (stake 40.0)"#]];
483            expected_message.assert_eq(error_object.message());
484            let expected_data = expect![[
485                r#"{"4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi":[{"object_id":"0x0000000000000000000000000000000000000000000000000000000000000000","version":"0","digest":"11111111111111111111111111111111"}],"8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR":[{"object_id":"0x0000000000000000000000000000000000000000000000000000000000000000","version":"0","digest":"11111111111111111111111111111111"}]}"#
486            ]];
487            let actual_data = error_object.data().unwrap().to_string();
488            expected_data.assert_eq(&actual_data);
489        }
490
491        #[test]
492        fn test_non_recoverable_transaction_error() {
493            let quorum_driver_error = QuorumDriverError::NonRecoverableTransactionError {
494                errors: vec![
495                    (
496                        IotaError::UserInput {
497                            error: UserInputError::GasBalanceTooLow {
498                                gas_balance: 10,
499                                needed_gas_amount: 100,
500                            },
501                        },
502                        0,
503                        vec![],
504                    ),
505                    (
506                        IotaError::UserInput {
507                            error: UserInputError::ObjectVersionUnavailableForConsumption {
508                                provided_obj_ref: test_object_ref(),
509                                current_version: 10.into(),
510                            },
511                        },
512                        0,
513                        vec![],
514                    ),
515                ],
516            };
517
518            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
519
520            let error_object = error_object_from_rpc(rpc_error);
521            let expected_code = expect!["-32002"];
522            expected_code.assert_eq(&error_object.code().to_string());
523            let expected_message = expect![
524                "Transaction execution failed due to issues with transaction inputs, please review the errors and try again:\n- Balance of gas object 10 is lower than the needed amount: 100\n- Object ID 0x0000000000000000000000000000000000000000000000000000000000000000 Version 0 Digest 11111111111111111111111111111111 is not available for consumption, current version: 10"
525            ];
526            expected_message.assert_eq(error_object.message());
527            let expected_data = expect![[r#"{"reason":"non_recoverable_transaction_error"}"#]];
528            expected_data.assert_eq(&error_object.data().unwrap().to_string());
529        }
530
531        #[test]
532        fn test_non_recoverable_transaction_error_with_transient_errors() {
533            let quorum_driver_error = QuorumDriverError::NonRecoverableTransactionError {
534                errors: vec![
535                    (
536                        IotaError::UserInput {
537                            error: UserInputError::ObjectNotFound {
538                                object_id: test_object_ref().object_id,
539                                version: None,
540                            },
541                        },
542                        0,
543                        vec![],
544                    ),
545                    (
546                        IotaError::Rpc("Hello".to_string(), "Testing".to_string()),
547                        0,
548                        vec![],
549                    ),
550                ],
551            };
552
553            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
554
555            let error_object = error_object_from_rpc(rpc_error);
556            let expected_code = expect!["-32002"];
557            expected_code.assert_eq(&error_object.code().to_string());
558            let expected_message = expect![
559                "Transaction execution failed due to issues with transaction inputs, please review the errors and try again:\n- Could not find the referenced object 0x0000000000000000000000000000000000000000000000000000000000000000 at version None"
560            ];
561            expected_message.assert_eq(error_object.message());
562        }
563
564        #[test]
565        fn test_quorum_driver_internal_error() {
566            let quorum_driver_error =
567                QuorumDriverError::QuorumDriverInternal(IotaError::UnexpectedMessage);
568
569            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
570
571            let error_object = error_object_from_rpc(rpc_error);
572            let expected_code = expect!["-32603"];
573            expected_code.assert_eq(&error_object.code().to_string());
574            let expected_message = expect!["Internal error occurred while executing transaction."];
575            expected_message.assert_eq(error_object.message());
576        }
577
578        #[test]
579        fn test_system_overload() {
580            let quorum_driver_error = QuorumDriverError::SystemOverload {
581                overloaded_stake: 10,
582                errors: vec![(IotaError::UnexpectedMessage, 0, vec![])],
583            };
584
585            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
586
587            let error_object = error_object_from_rpc(rpc_error);
588            let expected_code = expect!["-32050"];
589            expected_code.assert_eq(&error_object.code().to_string());
590            let expected_message = expect![
591                "Transaction is not processed because 10 of validators by stake are overloaded with certificates pending execution."
592            ];
593            expected_message.assert_eq(error_object.message());
594            let expected_data = expect![[r#"{"reason":"system_overload"}"#]];
595            expected_data.assert_eq(&error_object.data().unwrap().to_string());
596        }
597
598        #[test]
599        fn test_system_overload_retry_after() {
600            let quorum_driver_error = QuorumDriverError::SystemOverloadRetryAfter {
601                overload_stake: 4000,
602                errors: vec![(IotaError::UnexpectedMessage, 0, vec![])],
603                retry_after_secs: 30,
604            };
605
606            let rpc_error: RpcError = Error::QuorumDriver(quorum_driver_error).into();
607
608            let error_object = error_object_from_rpc(rpc_error);
609            let expected_code = expect!["-32050"];
610            expected_code.assert_eq(&error_object.code().to_string());
611            let expected_message = expect![
612                "Transaction is not processed because 4000 of validators are overloaded and asked client to retry after 30."
613            ];
614            expected_message.assert_eq(error_object.message());
615            let expected_data =
616                expect![[r#"{"reason":"system_overload_retry_after","retry_after_secs":30}"#]];
617            expected_data.assert_eq(&error_object.data().unwrap().to_string());
618        }
619    }
620}