1use std::sync::LazyLock;
2
3use af_sui_types::ObjectId;
4use extension_traits::extension;
5use jsonrpsee::core::ClientError;
6use jsonrpsee::types::{ErrorCode, ErrorObject, ErrorObjectOwned};
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> {
45 match &self {
46 Self::Call(err_obj) => Some(err_obj),
47 _ => None,
48 }
49 }
50}
51
52#[extension(pub trait ErrorObjectExt)]
62impl<'a> ErrorObject<'a> {
63 const TRANSIENT_ERROR_CODE: i32 = -32050;
64 const TRANSACTION_EXECUTION_CLIENT_ERROR_CODE: i32 = -32002;
65
66 fn is_transient_error(&self) -> bool {
75 self.code() == Self::TRANSIENT_ERROR_CODE
76 }
77
78 fn is_execution_error(&self) -> bool {
97 self.code() == Self::TRANSACTION_EXECUTION_CLIENT_ERROR_CODE
98 }
99
100 fn is_object_unavailable_for_consumption(&self) -> bool {
111 OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX.is_match(self.message())
112 }
113
114 fn is_object_not_found(&self) -> bool {
116 if self.code() != ErrorCode::InvalidParams.code() {
117 return false;
118 }
119 OBJECT_NOT_FOUND_REGEX.is_match(self.message())
120 }
121
122 fn is_transaction_retried_success(&self) -> bool {
130 self.is_execution_error() && RETRIED_TRANSACTION_SUCCESS_REGEX.is_match(self.message())
131 }
132
133 fn as_object_not_found(&self) -> Option<(ObjectId, Option<u64>)> {
143 if self.code() != ErrorCode::InvalidParams.code() {
144 return None;
145 }
146 let captures = OBJECT_NOT_FOUND_REGEX.captures(self.message())?;
147 let version = captures.get(2).and_then(|c| c.as_str().parse().ok());
149 Some((captures.get(1)?.as_str().parse().ok()?, version))
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn object_unavailable_for_consumption_regex_builds() {
159 let _ = &*OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX;
160 }
161
162 #[test]
163 fn object_not_found_regex_builds() {
164 let _ = &*OBJECT_NOT_FOUND_REGEX;
165 }
166
167 #[test]
169 fn object_unavailable_for_consumption_match1() {
170 let expect = "Transaction validator signing failed due to issues with transaction inputs, \
171 please review the errors and try again:\n\
172 - Balance of gas object 10 is lower than the needed amount: 100\n\
173 - Object ID 0x0000000000000000000000000000000000000000000000000000000000000000 \
174 Version 0x0 \
175 Digest 11111111111111111111111111111111 \
176 is not available for consumption, current version: 0xa";
177 assert!(OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX.is_match(expect));
178 }
179
180 #[test]
182 fn object_unavailable_for_consumption_match2() {
183 let expect = "Transaction validator signing failed due to issues with transaction inputs, \
184 please review the errors and try again:\n\
185 - Object ID 0xa3b25765e4f7f4524367fa792b608483157bfef919108f0d998c6980493fc7bc \
186 Version 0xb0cb7f7 \
187 Digest FKkELfAR3vP19MrjwEwTapH3JDbdZuxqcC25CoALNUsN \
188 is not available for consumption, current version: 0xb0cb7f8";
189 assert!(OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX.is_match(expect));
190 }
191
192 #[test]
194 fn object_unavailable_for_consumption_not_match1() {
195 let expect = "Transaction validator signing failed due to issues with transaction inputs, \
196 please review the errors and try again:\n\
197 - Transaction was not signed by the correct sender: \
198 Object 0x2d13a698a9ef878372210f6d96e2315f368794e1c9c842f6fddacb3815ff749d is owned \
199 by account address \
200 0x162602a3f40fcab9b513a3fefad1c046ae242bb6bb83334b3aa8cd639e018b28, but given \
201 owner/signer address is \
202 0x76f9ca7f89994d4039b739859a41b39123d6f695a1b33f7431cee3c6b40a45c2";
203 assert!(!OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX.is_match(expect));
204 }
205
206 #[test]
214 fn object_not_found_match1() {
215 let expect = "Error checking transaction input objects: ObjectNotFound { object_id: 0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a, version: Some(SequenceNumber(247815626)) }";
216 assert!(OBJECT_NOT_FOUND_REGEX.is_match(expect));
217 let matches = OBJECT_NOT_FOUND_REGEX
218 .captures(expect)
219 .expect("object_id and version present");
220 assert_eq!(
221 matches.get(1).expect("object_id").as_str(),
222 "0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a"
223 );
224 assert_eq!(matches.get(2).expect("version").as_str(), "247815626");
225 }
226
227 #[test]
235 fn object_not_found_match2() {
236 let expect = "Error checking transaction input objects: ObjectNotFound { object_id: 0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a, version: None }";
237 assert!(OBJECT_NOT_FOUND_REGEX.is_match(expect));
238 let matches = OBJECT_NOT_FOUND_REGEX
239 .captures(expect)
240 .expect("object_id present");
241 dbg!(&matches);
242 assert_eq!(
243 matches.get(1).expect("object_id").as_str(),
244 "0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a"
245 );
246 assert_eq!(matches.get(2), None);
247 }
248
249 #[test]
250 fn object_not_found1() {
251 let error = ErrorObject::owned::<()>(
252 ErrorCode::InvalidParams.code(),
253 "Error checking transaction input objects: ObjectNotFound { object_id: 0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a, version: Some(SequenceNumber(247815626)) }",
254 None,
255 );
256 assert!(error.is_object_not_found());
257 assert_eq!(
258 error.as_object_not_found(),
259 Some((
260 "0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a"
261 .parse()
262 .unwrap(),
263 Some(247815626)
264 ))
265 )
266 }
267
268 #[test]
269 fn object_not_found2() {
270 let error = ErrorObject::owned::<()>(
271 ErrorCode::InvalidParams.code(),
272 "Error checking transaction input objects: ObjectNotFound { object_id: 0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a, version: None }",
273 None,
274 );
275 assert!(error.is_object_not_found());
276 assert_eq!(
277 error.as_object_not_found(),
278 Some((
279 "0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a"
280 .parse()
281 .unwrap(),
282 None
283 ))
284 )
285 }
286
287 #[test]
288 fn retried_transaction_regex_matches() {
289 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)"#;
290 assert!(RETRIED_TRANSACTION_SUCCESS_REGEX.is_match(message));
291 }
292}