use {crate::sui, std::time::Duration, thiserror::Error};
pub const SDK_BUILD_IDENTITY: &str = env!("CARGO_PKG_VERSION");
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[error(
"object '{object_id}' uses witness '{witness_type}' and inner type '{inner}', which SDK build \
'{sdk_build}' does not support",
inner = inner_type
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(|| "<unavailable>".to_owned())
)]
pub struct ClientUpgradeRequired {
pub object_id: sui::types::Address,
pub witness_type: Box<sui::types::StructTag>,
pub inner_type: Option<Box<sui::types::StructTag>>,
pub sdk_build: &'static str,
}
impl ClientUpgradeRequired {
pub fn new(
object_id: sui::types::Address,
witness_type: sui::types::StructTag,
inner_type: Option<sui::types::StructTag>,
) -> Self {
Self {
object_id,
witness_type: Box::new(witness_type),
inner_type: inner_type.map(Box::new),
sdk_build: SDK_BUILD_IDENTITY,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransactionErrorState {
SubmissionRejected,
SubmissionUnknown,
ConfirmationUnknown,
ExecutionFailed,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum TransactionError {
#[error("transaction {digest} was rejected before execution: {source}")]
SubmissionRejected {
digest: crate::sui::types::Digest,
#[source]
source: tonic::Status,
},
#[error("transaction {digest} submission state is unknown: {source}")]
SubmissionUnknown {
digest: crate::sui::types::Digest,
#[source]
source: tonic::Status,
},
#[error(
"transaction {digest} was executed but checkpoint confirmation is unknown after {timeout:?}"
)]
ConfirmationTimedOut {
digest: crate::sui::types::Digest,
timeout: Duration,
response: Box<crate::sui::grpc::ExecuteTransactionResponse>,
},
#[error("transaction {digest} was executed but checkpoint confirmation failed: {source}")]
ConfirmationFailed {
digest: crate::sui::types::Digest,
response: Box<crate::sui::grpc::ExecuteTransactionResponse>,
#[source]
source: tonic::Status,
},
#[error("transaction {digest} confirmation response is invalid: {message}")]
ConfirmationResponseInvalid {
digest: crate::sui::types::Digest,
response: Box<crate::sui::grpc::ExecuteTransactionResponse>,
message: &'static str,
},
#[error(
"transaction {digest} failed at checkpoint {checkpoint} in command {command:?}: {error:?}"
)]
ExecutionFailed {
digest: crate::sui::types::Digest,
checkpoint: u64,
error: crate::sui::types::ExecutionError,
command: Option<u64>,
},
}
impl TransactionError {
pub fn submission_rejected(digest: crate::sui::types::Digest, source: tonic::Status) -> Self {
Self::SubmissionRejected { digest, source }
}
pub fn submission_unknown(digest: crate::sui::types::Digest, source: tonic::Status) -> Self {
Self::SubmissionUnknown { digest, source }
}
pub fn confirmation_timed_out(
digest: crate::sui::types::Digest,
timeout: Duration,
response: crate::sui::grpc::ExecuteTransactionResponse,
) -> Self {
Self::ConfirmationTimedOut {
digest,
timeout,
response: Box::new(response),
}
}
pub fn confirmation_failed(
digest: crate::sui::types::Digest,
response: crate::sui::grpc::ExecuteTransactionResponse,
source: tonic::Status,
) -> Self {
Self::ConfirmationFailed {
digest,
response: Box::new(response),
source,
}
}
pub fn confirmation_response_invalid(
digest: crate::sui::types::Digest,
response: crate::sui::grpc::ExecuteTransactionResponse,
message: &'static str,
) -> Self {
Self::ConfirmationResponseInvalid {
digest,
response: Box::new(response),
message,
}
}
pub fn execution_failed(
digest: crate::sui::types::Digest,
checkpoint: u64,
error: crate::sui::types::ExecutionError,
command: Option<u64>,
) -> Self {
Self::ExecutionFailed {
digest,
checkpoint,
error,
command,
}
}
pub fn digest(&self) -> &crate::sui::types::Digest {
match self {
Self::SubmissionRejected { digest, .. }
| Self::SubmissionUnknown { digest, .. }
| Self::ConfirmationTimedOut { digest, .. }
| Self::ConfirmationFailed { digest, .. }
| Self::ConfirmationResponseInvalid { digest, .. }
| Self::ExecutionFailed { digest, .. } => digest,
}
}
pub fn state(&self) -> TransactionErrorState {
match self {
Self::SubmissionRejected { .. } => TransactionErrorState::SubmissionRejected,
Self::SubmissionUnknown { .. } => TransactionErrorState::SubmissionUnknown,
Self::ConfirmationTimedOut { .. }
| Self::ConfirmationFailed { .. }
| Self::ConfirmationResponseInvalid { .. } => {
TransactionErrorState::ConfirmationUnknown
}
Self::ExecutionFailed { .. } => TransactionErrorState::ExecutionFailed,
}
}
pub fn response(&self) -> Option<&crate::sui::grpc::ExecuteTransactionResponse> {
match self {
Self::ConfirmationTimedOut { response, .. }
| Self::ConfirmationFailed { response, .. }
| Self::ConfirmationResponseInvalid { response, .. } => Some(response),
Self::SubmissionRejected { .. }
| Self::SubmissionUnknown { .. }
| Self::ExecutionFailed { .. } => None,
}
}
pub fn submission_rejection(&self) -> Option<&tonic::Status> {
match self {
Self::SubmissionRejected { source, .. } => Some(source),
Self::SubmissionUnknown { .. }
| Self::ConfirmationTimedOut { .. }
| Self::ConfirmationFailed { .. }
| Self::ConfirmationResponseInvalid { .. }
| Self::ExecutionFailed { .. } => None,
}
}
}
#[derive(Debug, Error)]
pub enum NexusError {
#[error(transparent)]
Transaction(Box<TransactionError>),
#[error("a private key is required for this operation")]
MissingPrivateKey,
#[error("Sui wallet error: {0}")]
Wallet(anyhow::Error),
#[error("Client configuration error: {0}")]
Configuration(String),
#[error("a gas source is already configured")]
GasSourceAlreadyConfigured,
#[error("Transaction building error: {0}")]
TransactionBuilding(anyhow::Error),
#[error("RPC error: {0}")]
Rpc(anyhow::Error),
#[error("Parsing error: {0}")]
Parsing(anyhow::Error),
#[error("Timeout error: {0}")]
Timeout(anyhow::Error),
#[error("Channel error: {0}")]
Channel(anyhow::Error),
#[error("Storage error: {0}")]
Storage(anyhow::Error),
#[error("Connected chain '{actual}' does not match configured chain '{expected}'")]
ChainMismatch { expected: String, actual: String },
#[error("Object state for '{object}' is invalid: {reason}")]
InvalidObjectState {
object: sui::types::Address,
reason: String,
},
#[error("Transaction '{transaction}' outputs are invalid: {reason}")]
InvalidTransactionOutput {
transaction: sui::types::Digest,
reason: String,
},
#[error("Package '{package}' is incompatible: {reason}")]
IncompatiblePackage {
package: sui::types::Address,
reason: String,
},
#[error("Object '{object}' does not exist")]
ObjectNotFound { object: sui::types::Address },
#[error(transparent)]
ClientUpgradeRequired(#[from] ClientUpgradeRequired),
}
impl NexusError {
pub fn from_rpc(error: anyhow::Error) -> Self {
match error.downcast_ref::<Self>() {
Some(Self::ObjectNotFound { object }) => Self::ObjectNotFound { object: *object },
_ => Self::Rpc(error),
}
}
}
impl From<TransactionError> for NexusError {
fn from(error: TransactionError) -> Self {
Self::Transaction(Box::new(error))
}
}
#[cfg(test)]
mod tests {
use {
super::{TransactionError, TransactionErrorState},
crate::sui,
std::time::Duration,
};
#[test]
fn submission_unknown_retains_transaction_identity() {
let digest = sui::types::Digest::new([7; 32]);
let error = TransactionError::submission_unknown(
digest,
tonic::Status::unavailable("connection closed"),
);
assert_eq!(error.digest(), &digest);
assert_eq!(error.state(), TransactionErrorState::SubmissionUnknown);
assert!(error.response().is_none());
assert!(!error.to_string().contains("retry"));
}
#[test]
fn submission_rejection_is_known_and_retains_transaction_identity() {
let digest = sui::types::Digest::new([7; 32]);
let error = TransactionError::submission_rejected(
digest,
tonic::Status::invalid_argument("invalid gas reservation"),
);
assert_eq!(error.digest(), &digest);
assert_eq!(error.state(), TransactionErrorState::SubmissionRejected);
assert!(error.response().is_none());
assert_eq!(
error.submission_rejection().map(tonic::Status::message),
Some("invalid gas reservation")
);
assert!(!error.to_string().contains("unknown"));
}
#[test]
fn confirmation_unknown_retains_transaction_identity_and_response() {
let digest = sui::types::Digest::new([7; 32]);
let error = TransactionError::confirmation_timed_out(
digest,
Duration::from_secs(30),
sui::grpc::ExecuteTransactionResponse::default(),
);
assert_eq!(error.digest(), &digest);
assert_eq!(error.state(), TransactionErrorState::ConfirmationUnknown);
assert!(error.response().is_some());
assert!(!error.to_string().contains("retry"));
}
#[test]
fn invalid_confirmation_response_retains_transaction_identity_and_response() {
let digest = sui::types::Digest::new([7; 32]);
let response = sui::grpc::ExecuteTransactionResponse::default();
let error = TransactionError::confirmation_response_invalid(
digest,
response.clone(),
"transaction is missing",
);
assert_eq!(error.digest(), &digest);
assert_eq!(error.state(), TransactionErrorState::ConfirmationUnknown);
assert_eq!(error.response(), Some(&response));
assert!(!error.to_string().contains("retry"));
}
#[test]
fn execution_failure_retains_transaction_identity() {
let digest = sui::types::Digest::new([7; 32]);
let error = TransactionError::execution_failed(
digest,
42,
sui::types::ExecutionError::InsufficientGas,
Some(3),
);
assert_eq!(error.digest(), &digest);
assert_eq!(error.state(), TransactionErrorState::ExecutionFailed);
assert!(error.response().is_none());
assert!(!error.to_string().contains("retry"));
}
}