sui_jsonrpc/
error.rs

1use std::sync::LazyLock;
2
3use extension_traits::extension;
4use jsonrpsee_core::ClientError;
5use jsonrpsee_types::{ErrorCode, ErrorObject, ErrorObjectOwned};
6use sui_sdk_types::Address;
7
8pub type JsonRpcClientResult<T = ()> = Result<T, JsonRpcClientError>;
9
10pub type JsonRpcClientError = ClientError;
11
12static OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
13    regex::RegexBuilder::new("Object .* not available for consumption, current version:")
14        .case_insensitive(true)
15        .build()
16        .expect("Tested below for panics")
17});
18
19/// Breakdown:
20/// - "object_id: ([[:alnum:]]+)" captures the object id
21/// - "version: (?:...|None)" matches either the first pattern or "None", but doesn't capture its
22///   content
23/// - "Some\(SequenceNumber\((\d+)\)\)": captures the object version
24static OBJECT_NOT_FOUND_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
25    regex::RegexBuilder::new(r"Error checking transaction input objects: ObjectNotFound \{ object_id: ([[:alnum:]]+), version: (?:Some\(SequenceNumber\((\d+)\)\)|None) \}")
26        .case_insensitive(true)
27        .build()
28        .expect("Tested below for panics")
29});
30
31static RETRIED_TRANSACTION_SUCCESS_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
32    regex::RegexBuilder::new(r"Failed to sign transaction by a quorum of validators because one or more of its objects is reserved for another transaction. Retried transaction [[:word:]]+ \(succeeded\) because it was able to gather the necessary votes.")
33        .case_insensitive(true)
34        .build()
35        .expect("Tested below for panics")
36});
37
38/// Helpers to inspect the error type from the client implementation.
39#[extension(pub trait JsonRpcClientErrorExt)]
40impl JsonRpcClientError {
41    /// If this is a JSON-RPC [error object], return a reference to it.
42    ///
43    /// If submitting a transaction, these are usually pre-execution validity check failures, e.g.,
44    /// wrong signatures, equivocated objects, etc. Use [`ErrorObjectExt`] to extract more
45    /// information from those.
46    ///
47    /// [error object]: https://www.jsonrpc.org/specification#response_object
48    fn as_error_object(&self) -> Option<&ErrorObjectOwned> {
49        match &self {
50            Self::Call(err_obj) => Some(err_obj),
51            _ => None,
52        }
53    }
54}
55
56/// Helpers to inspect the error type from JSON-RPC calls.
57///
58/// JSON-RPC [error object] codes taken from the [original implementation].
59///
60/// See the [source] for codes and messages in the quorum driver FN API.
61///
62/// [error object]: https://www.jsonrpc.org/specification#response_object
63/// [original implementation]: https://github.com/MystenLabs/sui/blob/main/crates/sui-json-rpc-api/src/lib.rs
64/// [source]: https://github.com/MystenLabs/sui/blob/testnet-v1.35.1/crates/sui-json-rpc/src/error.rs
65#[extension(pub trait ErrorObjectExt)]
66impl<'a> ErrorObject<'a> {
67    const TRANSIENT_ERROR_CODE: i32 = -32050;
68    const TRANSACTION_EXECUTION_CLIENT_ERROR_CODE: i32 = -32002;
69
70    /// Transient error, suggesting it may be possible to retry.
71    ///
72    /// # Example error messages
73    ///
74    /// - Transaction timed out before reaching finality
75    /// - Transaction failed to reach finality with transient error after X attempts
76    /// - Transaction is not processed because [...] of validators by stake are overloaded with
77    ///   certificates pending execution.
78    fn is_transient_error(&self) -> bool {
79        self.code() == Self::TRANSIENT_ERROR_CODE
80    }
81
82    /// Error in transaction execution (pre-consensus)
83    ///
84    /// # Example error messages
85    ///
86    /// - Invalid user signature
87    /// - Failed to sign transaction by a quorum of validators because one or more of its objects
88    ///   is {reason}. {retried_info} Other transactions locking these objects: [...]
89    ///   - reason:
90    ///     - equivocated until the next epoch
91    ///     - reserved for another transaction
92    ///   - retried_info: Retried transaction [...] (success/failure) because it was able to
93    ///     gather the necessary votes
94    /// - Transaction validator signing failed due to issues with transaction inputs, please review
95    ///   the errors and try again: {reason}
96    ///   - reason:
97    ///     - Balance of gas object [...] is lower than the needed amount
98    ///     - Object [...] not available for consumption, current version: [...]
99    ///     - Could not find the referenced object [...] at version [...]
100    fn is_execution_error(&self) -> bool {
101        self.code() == Self::TRANSACTION_EXECUTION_CLIENT_ERROR_CODE
102    }
103
104    /// Error with message "Object [...] not available for consumption, current version: [...]".
105    ///
106    /// TLDR: usually happens when the client's state sync is lagging too much.
107    ///
108    /// Note that this may not be the single reason why the transaction failed. Other errors
109    /// related to the transaction inputs may be present.
110    ///
111    /// May be due to state sync lag. For example, the client submits two transactions in quick
112    /// succession but doesn't sync owned objects quick enough for the second transaction, therefore
113    /// it uses the same owned object reference twice.
114    fn is_object_unavailable_for_consumption(&self) -> bool {
115        OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX.is_match(self.message())
116    }
117
118    /// Like [`Self::as_object_not_found`], but doesn't extract the object id and version.
119    fn is_object_not_found(&self) -> bool {
120        if self.code() != ErrorCode::InvalidParams.code() {
121            return false;
122        }
123        OBJECT_NOT_FOUND_REGEX.is_match(self.message())
124    }
125
126    /// Whether the transaction didn't fail because the RPC retried the submission and it
127    /// succeeded.
128    ///
129    /// The message for such an error looks like:
130    /// ```text
131    /// Failed to sign transaction by a quorum of validators because one or more of its objects is reserved for another transaction. Retried transaction [...] (succeeded) because it was able to gather the necessary votes
132    /// ```
133    fn is_transaction_retried_success(&self) -> bool {
134        self.is_execution_error() && RETRIED_TRANSACTION_SUCCESS_REGEX.is_match(self.message())
135    }
136
137    /// Error with [`InvalidParams`] code and message "Error checking transaction input objects:
138    /// ObjectNotFound { ... }"
139    ///
140    /// TLDR: usually happens when the client's state sync is faster than the full node's.
141    ///
142    /// Seen in practice when dry-running a transaction and the full node hasn't synchronized yet
143    /// with the effects of a previous one.
144    ///
145    /// [`InvalidParams`]: ErrorCode::InvalidParams
146    fn as_object_not_found(&self) -> Option<(Address, Option<u64>)> {
147        if self.code() != ErrorCode::InvalidParams.code() {
148            return None;
149        }
150        let captures = OBJECT_NOT_FOUND_REGEX.captures(self.message())?;
151        // Version may be None so we don't use `?` after `.get()`
152        let version = captures.get(2).and_then(|c| c.as_str().parse().ok());
153        Some((captures.get(1)?.as_str().parse().ok()?, version))
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn object_unavailable_for_consumption_regex_builds() {
163        let _ = &*OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX;
164    }
165
166    #[test]
167    fn object_not_found_regex_builds() {
168        let _ = &*OBJECT_NOT_FOUND_REGEX;
169    }
170
171    /// Toy example
172    #[test]
173    fn object_unavailable_for_consumption_match1() {
174        let expect = "Transaction validator signing failed due to issues with transaction inputs, \
175            please review the errors and try again:\n\
176            - Balance of gas object 10 is lower than the needed amount: 100\n\
177            - Object ID 0x0000000000000000000000000000000000000000000000000000000000000000 \
178              Version 0x0 \
179              Digest 11111111111111111111111111111111 \
180              is not available for consumption, current version: 0xa";
181        assert!(OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX.is_match(expect));
182    }
183
184    /// Real-world example
185    #[test]
186    fn object_unavailable_for_consumption_match2() {
187        let expect = "Transaction validator signing failed due to issues with transaction inputs, \
188            please review the errors and try again:\n\
189            - Object ID 0xa3b25765e4f7f4524367fa792b608483157bfef919108f0d998c6980493fc7bc \
190              Version 0xb0cb7f7 \
191              Digest FKkELfAR3vP19MrjwEwTapH3JDbdZuxqcC25CoALNUsN \
192              is not available for consumption, current version: 0xb0cb7f8";
193        assert!(OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX.is_match(expect));
194    }
195
196    /// Real-world example
197    #[test]
198    fn object_unavailable_for_consumption_not_match1() {
199        let expect = "Transaction validator signing failed due to issues with transaction inputs, \
200            please review the errors and try again:\n\
201            - Transaction was not signed by the correct sender: \
202              Object 0x2d13a698a9ef878372210f6d96e2315f368794e1c9c842f6fddacb3815ff749d is owned \
203              by account address \
204              0x162602a3f40fcab9b513a3fefad1c046ae242bb6bb83334b3aa8cd639e018b28, but given \
205              owner/signer address is \
206              0x76f9ca7f89994d4039b739859a41b39123d6f695a1b33f7431cee3c6b40a45c2";
207        assert!(!OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX.is_match(expect));
208    }
209
210    /// Real-world example
211    ///
212    /// ErrorObject {
213    ///     code: InvalidParams,
214    ///     message: "Error checking transaction input objects: ObjectNotFound { object_id: 0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a, version: Some(SequenceNumber(247815626)) }",
215    ///     data: None
216    /// }
217    #[test]
218    fn object_not_found_match1() {
219        let expect = "Error checking transaction input objects: ObjectNotFound { object_id: 0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a, version: Some(SequenceNumber(247815626)) }";
220        assert!(OBJECT_NOT_FOUND_REGEX.is_match(expect));
221        let matches = OBJECT_NOT_FOUND_REGEX
222            .captures(expect)
223            .expect("object_id and version present");
224        assert_eq!(
225            matches.get(1).expect("object_id").as_str(),
226            "0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a"
227        );
228        assert_eq!(matches.get(2).expect("version").as_str(), "247815626");
229    }
230
231    /// Hypothetical
232    ///
233    /// ErrorObject {
234    ///     code: InvalidParams,
235    ///     message: "Error checking transaction input objects: ObjectNotFound { object_id: 0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a, version: None }",
236    ///     data: None
237    /// }
238    #[test]
239    fn object_not_found_match2() {
240        let expect = "Error checking transaction input objects: ObjectNotFound { object_id: 0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a, version: None }";
241        assert!(OBJECT_NOT_FOUND_REGEX.is_match(expect));
242        let matches = OBJECT_NOT_FOUND_REGEX
243            .captures(expect)
244            .expect("object_id present");
245        dbg!(&matches);
246        assert_eq!(
247            matches.get(1).expect("object_id").as_str(),
248            "0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a"
249        );
250        assert_eq!(matches.get(2), None);
251    }
252
253    #[test]
254    fn object_not_found1() {
255        let error = ErrorObject::owned::<()>(
256            ErrorCode::InvalidParams.code(),
257            "Error checking transaction input objects: ObjectNotFound { object_id: 0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a, version: Some(SequenceNumber(247815626)) }",
258            None,
259        );
260        assert!(error.is_object_not_found());
261        assert_eq!(
262            error.as_object_not_found(),
263            Some((
264                "0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a"
265                    .parse()
266                    .unwrap(),
267                Some(247815626)
268            ))
269        )
270    }
271
272    #[test]
273    fn object_not_found2() {
274        let error = ErrorObject::owned::<()>(
275            ErrorCode::InvalidParams.code(),
276            "Error checking transaction input objects: ObjectNotFound { object_id: 0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a, version: None }",
277            None,
278        );
279        assert!(error.is_object_not_found());
280        assert_eq!(
281            error.as_object_not_found(),
282            Some((
283                "0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a"
284                    .parse()
285                    .unwrap(),
286                None
287            ))
288        )
289    }
290
291    #[test]
292    fn retried_transaction_regex_matches() {
293        let message = r#"Failed to sign transaction by a quorum of validators because one or more of its objects is reserved for another transaction. Retried transaction EENuLfRygexZrE1ycfsUHRFFenAgPLReFxYTFKQg9Jpu (succeeded) because it was able to gather the necessary votes. Other transactions locking these objects:\n- EENuLfRygexZrE1ycfsUHRFFenAgPLReFxYTFKQg9Jpu (stake 90.17)"#;
294        assert!(RETRIED_TRANSACTION_SUCCESS_REGEX.is_match(message));
295    }
296}