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
19static 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#[extension(pub trait JsonRpcClientErrorExt)]
40impl JsonRpcClientError {
41 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#[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 fn is_transient_error(&self) -> bool {
79 self.code() == Self::TRANSIENT_ERROR_CODE
80 }
81
82 fn is_execution_error(&self) -> bool {
101 self.code() == Self::TRANSACTION_EXECUTION_CLIENT_ERROR_CODE
102 }
103
104 fn is_object_unavailable_for_consumption(&self) -> bool {
115 OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX.is_match(self.message())
116 }
117
118 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 fn is_transaction_retried_success(&self) -> bool {
134 self.is_execution_error() && RETRIED_TRANSACTION_SUCCESS_REGEX.is_match(self.message())
135 }
136
137 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 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 #[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 #[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 #[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 #[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 #[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}