use std::sync::LazyLock;
use extension_traits::extension;
use jsonrpsee_core::ClientError;
use jsonrpsee_types::{ErrorCode, ErrorObject, ErrorObjectOwned};
use sui_sdk_types::Address;
pub type JsonRpcClientResult<T = ()> = Result<T, JsonRpcClientError>;
pub type JsonRpcClientError = ClientError;
static OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::RegexBuilder::new("Object .* not available for consumption, current version:")
.case_insensitive(true)
.build()
.expect("Tested below for panics")
});
static OBJECT_NOT_FOUND_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::RegexBuilder::new(r"Error checking transaction input objects: ObjectNotFound \{ object_id: ([[:alnum:]]+), version: (?:Some\(SequenceNumber\((\d+)\)\)|None) \}")
.case_insensitive(true)
.build()
.expect("Tested below for panics")
});
static RETRIED_TRANSACTION_SUCCESS_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
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.")
.case_insensitive(true)
.build()
.expect("Tested below for panics")
});
#[extension(pub trait JsonRpcClientErrorExt)]
impl JsonRpcClientError {
fn as_error_object(&self) -> Option<&ErrorObjectOwned> {
match &self {
Self::Call(err_obj) => Some(err_obj),
_ => None,
}
}
}
#[extension(pub trait ErrorObjectExt)]
impl<'a> ErrorObject<'a> {
const TRANSIENT_ERROR_CODE: i32 = -32050;
const TRANSACTION_EXECUTION_CLIENT_ERROR_CODE: i32 = -32002;
fn is_transient_error(&self) -> bool {
self.code() == Self::TRANSIENT_ERROR_CODE
}
fn is_execution_error(&self) -> bool {
self.code() == Self::TRANSACTION_EXECUTION_CLIENT_ERROR_CODE
}
fn is_object_unavailable_for_consumption(&self) -> bool {
OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX.is_match(self.message())
}
fn is_object_not_found(&self) -> bool {
if self.code() != ErrorCode::InvalidParams.code() {
return false;
}
OBJECT_NOT_FOUND_REGEX.is_match(self.message())
}
fn is_transaction_retried_success(&self) -> bool {
self.is_execution_error() && RETRIED_TRANSACTION_SUCCESS_REGEX.is_match(self.message())
}
fn as_object_not_found(&self) -> Option<(Address, Option<u64>)> {
if self.code() != ErrorCode::InvalidParams.code() {
return None;
}
let captures = OBJECT_NOT_FOUND_REGEX.captures(self.message())?;
let version = captures.get(2).and_then(|c| c.as_str().parse().ok());
Some((captures.get(1)?.as_str().parse().ok()?, version))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn object_unavailable_for_consumption_regex_builds() {
let _ = &*OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX;
}
#[test]
fn object_not_found_regex_builds() {
let _ = &*OBJECT_NOT_FOUND_REGEX;
}
#[test]
fn object_unavailable_for_consumption_match1() {
let expect = "Transaction validator signing 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 0x0 \
Digest 11111111111111111111111111111111 \
is not available for consumption, current version: 0xa";
assert!(OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX.is_match(expect));
}
#[test]
fn object_unavailable_for_consumption_match2() {
let expect = "Transaction validator signing failed due to issues with transaction inputs, \
please review the errors and try again:\n\
- Object ID 0xa3b25765e4f7f4524367fa792b608483157bfef919108f0d998c6980493fc7bc \
Version 0xb0cb7f7 \
Digest FKkELfAR3vP19MrjwEwTapH3JDbdZuxqcC25CoALNUsN \
is not available for consumption, current version: 0xb0cb7f8";
assert!(OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX.is_match(expect));
}
#[test]
fn object_unavailable_for_consumption_not_match1() {
let expect = "Transaction validator signing failed due to issues with transaction inputs, \
please review the errors and try again:\n\
- Transaction was not signed by the correct sender: \
Object 0x2d13a698a9ef878372210f6d96e2315f368794e1c9c842f6fddacb3815ff749d is owned \
by account address \
0x162602a3f40fcab9b513a3fefad1c046ae242bb6bb83334b3aa8cd639e018b28, but given \
owner/signer address is \
0x76f9ca7f89994d4039b739859a41b39123d6f695a1b33f7431cee3c6b40a45c2";
assert!(!OBJECT_UNAVAILABLE_FOR_CONSUMPTION_REGEX.is_match(expect));
}
#[test]
fn object_not_found_match1() {
let expect = "Error checking transaction input objects: ObjectNotFound { object_id: 0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a, version: Some(SequenceNumber(247815626)) }";
assert!(OBJECT_NOT_FOUND_REGEX.is_match(expect));
let matches = OBJECT_NOT_FOUND_REGEX
.captures(expect)
.expect("object_id and version present");
assert_eq!(
matches.get(1).expect("object_id").as_str(),
"0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a"
);
assert_eq!(matches.get(2).expect("version").as_str(), "247815626");
}
#[test]
fn object_not_found_match2() {
let expect = "Error checking transaction input objects: ObjectNotFound { object_id: 0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a, version: None }";
assert!(OBJECT_NOT_FOUND_REGEX.is_match(expect));
let matches = OBJECT_NOT_FOUND_REGEX
.captures(expect)
.expect("object_id present");
dbg!(&matches);
assert_eq!(
matches.get(1).expect("object_id").as_str(),
"0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a"
);
assert_eq!(matches.get(2), None);
}
#[test]
fn object_not_found1() {
let error = ErrorObject::owned::<()>(
ErrorCode::InvalidParams.code(),
"Error checking transaction input objects: ObjectNotFound { object_id: 0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a, version: Some(SequenceNumber(247815626)) }",
None,
);
assert!(error.is_object_not_found());
assert_eq!(
error.as_object_not_found(),
Some((
"0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a"
.parse()
.unwrap(),
Some(247815626)
))
)
}
#[test]
fn object_not_found2() {
let error = ErrorObject::owned::<()>(
ErrorCode::InvalidParams.code(),
"Error checking transaction input objects: ObjectNotFound { object_id: 0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a, version: None }",
None,
);
assert!(error.is_object_not_found());
assert_eq!(
error.as_object_not_found(),
Some((
"0x38826d19eb0338509eedf78c4f3a1de6479163e1a0a2fb447aa3fe947ef4cc2a"
.parse()
.unwrap(),
None
))
)
}
#[test]
fn retried_transaction_regex_matches() {
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)"#;
assert!(RETRIED_TRANSACTION_SUCCESS_REGEX.is_match(message));
}
}