1use {crate::sui, std::time::Duration, thiserror::Error};
4
5pub const SDK_BUILD_IDENTITY: &str = env!("CARGO_PKG_VERSION");
7
8#[derive(Clone, Debug, Eq, Error, PartialEq)]
10#[error(
11 "object '{object_id}' uses witness '{witness_type}' and inner type '{inner}', which SDK build \
12 '{sdk_build}' does not support",
13 inner = inner_type
14 .as_ref()
15 .map(ToString::to_string)
16 .unwrap_or_else(|| "<unavailable>".to_owned())
17)]
18pub struct ClientUpgradeRequired {
19 pub object_id: sui::types::Address,
21 pub witness_type: Box<sui::types::StructTag>,
23 pub inner_type: Option<Box<sui::types::StructTag>>,
25 pub sdk_build: &'static str,
27}
28
29impl ClientUpgradeRequired {
30 pub fn new(
32 object_id: sui::types::Address,
33 witness_type: sui::types::StructTag,
34 inner_type: Option<sui::types::StructTag>,
35 ) -> Self {
36 Self {
37 object_id,
38 witness_type: Box::new(witness_type),
39 inner_type: inner_type.map(Box::new),
40 sdk_build: SDK_BUILD_IDENTITY,
41 }
42 }
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum TransactionErrorState {
48 SubmissionRejected,
50 SubmissionUnknown,
52 ConfirmationUnknown,
54 ExecutionFailed,
56}
57
58#[derive(Debug, Error)]
60#[non_exhaustive]
61pub enum TransactionError {
62 #[error("transaction {digest} was rejected before execution: {source}")]
64 SubmissionRejected {
65 digest: crate::sui::types::Digest,
66 #[source]
67 source: tonic::Status,
68 },
69 #[error("transaction {digest} submission state is unknown: {source}")]
71 SubmissionUnknown {
72 digest: crate::sui::types::Digest,
73 #[source]
74 source: tonic::Status,
75 },
76 #[error(
78 "transaction {digest} was executed but checkpoint confirmation is unknown after {timeout:?}"
79 )]
80 ConfirmationTimedOut {
81 digest: crate::sui::types::Digest,
82 timeout: Duration,
83 response: Box<crate::sui::grpc::ExecuteTransactionResponse>,
84 },
85 #[error("transaction {digest} was executed but checkpoint confirmation failed: {source}")]
87 ConfirmationFailed {
88 digest: crate::sui::types::Digest,
89 response: Box<crate::sui::grpc::ExecuteTransactionResponse>,
90 #[source]
91 source: tonic::Status,
92 },
93 #[error("transaction {digest} confirmation response is invalid: {message}")]
95 ConfirmationResponseInvalid {
96 digest: crate::sui::types::Digest,
97 response: Box<crate::sui::grpc::ExecuteTransactionResponse>,
98 message: &'static str,
99 },
100 #[error(
102 "transaction {digest} failed at checkpoint {checkpoint} in command {command:?}: {error:?}"
103 )]
104 ExecutionFailed {
105 digest: crate::sui::types::Digest,
106 checkpoint: u64,
107 error: crate::sui::types::ExecutionError,
108 command: Option<u64>,
109 },
110}
111
112impl TransactionError {
113 pub fn submission_rejected(digest: crate::sui::types::Digest, source: tonic::Status) -> Self {
115 Self::SubmissionRejected { digest, source }
116 }
117
118 pub fn submission_unknown(digest: crate::sui::types::Digest, source: tonic::Status) -> Self {
120 Self::SubmissionUnknown { digest, source }
121 }
122
123 pub fn confirmation_timed_out(
125 digest: crate::sui::types::Digest,
126 timeout: Duration,
127 response: crate::sui::grpc::ExecuteTransactionResponse,
128 ) -> Self {
129 Self::ConfirmationTimedOut {
130 digest,
131 timeout,
132 response: Box::new(response),
133 }
134 }
135
136 pub fn confirmation_failed(
138 digest: crate::sui::types::Digest,
139 response: crate::sui::grpc::ExecuteTransactionResponse,
140 source: tonic::Status,
141 ) -> Self {
142 Self::ConfirmationFailed {
143 digest,
144 response: Box::new(response),
145 source,
146 }
147 }
148
149 pub fn confirmation_response_invalid(
151 digest: crate::sui::types::Digest,
152 response: crate::sui::grpc::ExecuteTransactionResponse,
153 message: &'static str,
154 ) -> Self {
155 Self::ConfirmationResponseInvalid {
156 digest,
157 response: Box::new(response),
158 message,
159 }
160 }
161
162 pub fn execution_failed(
164 digest: crate::sui::types::Digest,
165 checkpoint: u64,
166 error: crate::sui::types::ExecutionError,
167 command: Option<u64>,
168 ) -> Self {
169 Self::ExecutionFailed {
170 digest,
171 checkpoint,
172 error,
173 command,
174 }
175 }
176
177 pub fn digest(&self) -> &crate::sui::types::Digest {
179 match self {
180 Self::SubmissionRejected { digest, .. }
181 | Self::SubmissionUnknown { digest, .. }
182 | Self::ConfirmationTimedOut { digest, .. }
183 | Self::ConfirmationFailed { digest, .. }
184 | Self::ConfirmationResponseInvalid { digest, .. }
185 | Self::ExecutionFailed { digest, .. } => digest,
186 }
187 }
188
189 pub fn state(&self) -> TransactionErrorState {
191 match self {
192 Self::SubmissionRejected { .. } => TransactionErrorState::SubmissionRejected,
193 Self::SubmissionUnknown { .. } => TransactionErrorState::SubmissionUnknown,
194 Self::ConfirmationTimedOut { .. }
195 | Self::ConfirmationFailed { .. }
196 | Self::ConfirmationResponseInvalid { .. } => {
197 TransactionErrorState::ConfirmationUnknown
198 }
199 Self::ExecutionFailed { .. } => TransactionErrorState::ExecutionFailed,
200 }
201 }
202
203 pub fn response(&self) -> Option<&crate::sui::grpc::ExecuteTransactionResponse> {
205 match self {
206 Self::ConfirmationTimedOut { response, .. }
207 | Self::ConfirmationFailed { response, .. }
208 | Self::ConfirmationResponseInvalid { response, .. } => Some(response),
209 Self::SubmissionRejected { .. }
210 | Self::SubmissionUnknown { .. }
211 | Self::ExecutionFailed { .. } => None,
212 }
213 }
214
215 pub fn submission_rejection(&self) -> Option<&tonic::Status> {
218 match self {
219 Self::SubmissionRejected { source, .. } => Some(source),
220 Self::SubmissionUnknown { .. }
221 | Self::ConfirmationTimedOut { .. }
222 | Self::ConfirmationFailed { .. }
223 | Self::ConfirmationResponseInvalid { .. }
224 | Self::ExecutionFailed { .. } => None,
225 }
226 }
227}
228
229#[derive(Debug, Error)]
230pub enum NexusError {
231 #[error(transparent)]
232 Transaction(Box<TransactionError>),
233 #[error("a private key is required for this operation")]
234 MissingPrivateKey,
235 #[error("Sui wallet error: {0}")]
236 Wallet(anyhow::Error),
237 #[error("Client configuration error: {0}")]
238 Configuration(String),
239 #[error("a gas source is already configured")]
240 GasSourceAlreadyConfigured,
241 #[error("Transaction building error: {0}")]
242 TransactionBuilding(anyhow::Error),
243 #[error("RPC error: {0}")]
244 Rpc(anyhow::Error),
245 #[error("Parsing error: {0}")]
246 Parsing(anyhow::Error),
247 #[error("Timeout error: {0}")]
248 Timeout(anyhow::Error),
249 #[error("Channel error: {0}")]
250 Channel(anyhow::Error),
251 #[error("Storage error: {0}")]
252 Storage(anyhow::Error),
253 #[error("Connected chain '{actual}' does not match configured chain '{expected}'")]
255 ChainMismatch { expected: String, actual: String },
256 #[error("Object state for '{object}' is invalid: {reason}")]
258 InvalidObjectState {
259 object: sui::types::Address,
260 reason: String,
261 },
262 #[error("Transaction '{transaction}' outputs are invalid: {reason}")]
264 InvalidTransactionOutput {
265 transaction: sui::types::Digest,
266 reason: String,
267 },
268 #[error("Package '{package}' is incompatible: {reason}")]
270 IncompatiblePackage {
271 package: sui::types::Address,
272 reason: String,
273 },
274 #[error("Object '{object}' does not exist")]
276 ObjectNotFound { object: sui::types::Address },
277 #[error(transparent)]
279 ClientUpgradeRequired(#[from] ClientUpgradeRequired),
280}
281
282impl NexusError {
283 pub fn from_rpc(error: anyhow::Error) -> Self {
286 match error.downcast_ref::<Self>() {
287 Some(Self::ObjectNotFound { object }) => Self::ObjectNotFound { object: *object },
288 _ => Self::Rpc(error),
289 }
290 }
291}
292
293impl From<TransactionError> for NexusError {
294 fn from(error: TransactionError) -> Self {
295 Self::Transaction(Box::new(error))
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use {
302 super::{TransactionError, TransactionErrorState},
303 crate::sui,
304 std::time::Duration,
305 };
306
307 #[test]
308 fn submission_unknown_retains_transaction_identity() {
309 let digest = sui::types::Digest::new([7; 32]);
310 let error = TransactionError::submission_unknown(
311 digest,
312 tonic::Status::unavailable("connection closed"),
313 );
314
315 assert_eq!(error.digest(), &digest);
316 assert_eq!(error.state(), TransactionErrorState::SubmissionUnknown);
317 assert!(error.response().is_none());
318 assert!(!error.to_string().contains("retry"));
319 }
320
321 #[test]
322 fn submission_rejection_is_known_and_retains_transaction_identity() {
323 let digest = sui::types::Digest::new([7; 32]);
324 let error = TransactionError::submission_rejected(
325 digest,
326 tonic::Status::invalid_argument("invalid gas reservation"),
327 );
328
329 assert_eq!(error.digest(), &digest);
330 assert_eq!(error.state(), TransactionErrorState::SubmissionRejected);
331 assert!(error.response().is_none());
332 assert_eq!(
333 error.submission_rejection().map(tonic::Status::message),
334 Some("invalid gas reservation")
335 );
336 assert!(!error.to_string().contains("unknown"));
337 }
338
339 #[test]
340 fn confirmation_unknown_retains_transaction_identity_and_response() {
341 let digest = sui::types::Digest::new([7; 32]);
342 let error = TransactionError::confirmation_timed_out(
343 digest,
344 Duration::from_secs(30),
345 sui::grpc::ExecuteTransactionResponse::default(),
346 );
347
348 assert_eq!(error.digest(), &digest);
349 assert_eq!(error.state(), TransactionErrorState::ConfirmationUnknown);
350 assert!(error.response().is_some());
351 assert!(!error.to_string().contains("retry"));
352 }
353
354 #[test]
355 fn invalid_confirmation_response_retains_transaction_identity_and_response() {
356 let digest = sui::types::Digest::new([7; 32]);
357 let response = sui::grpc::ExecuteTransactionResponse::default();
358 let error = TransactionError::confirmation_response_invalid(
359 digest,
360 response.clone(),
361 "transaction is missing",
362 );
363
364 assert_eq!(error.digest(), &digest);
365 assert_eq!(error.state(), TransactionErrorState::ConfirmationUnknown);
366 assert_eq!(error.response(), Some(&response));
367 assert!(!error.to_string().contains("retry"));
368 }
369
370 #[test]
371 fn execution_failure_retains_transaction_identity() {
372 let digest = sui::types::Digest::new([7; 32]);
373 let error = TransactionError::execution_failed(
374 digest,
375 42,
376 sui::types::ExecutionError::InsufficientGas,
377 Some(3),
378 );
379
380 assert_eq!(error.digest(), &digest);
381 assert_eq!(error.state(), TransactionErrorState::ExecutionFailed);
382 assert!(error.response().is_none());
383 assert!(!error.to_string().contains("retry"));
384 }
385}