1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
use algonaut_abi::abi_error::AbiError;
use algonaut_transaction::error::TransactionError;
use std::fmt::Debug;
use std::time::Duration;
use thiserror::Error;
// `Error` is intentionally not `Clone`/`PartialEq`/`Eq`: it carries source
// errors (`TransactionError`, `AbiError`) via `#[from]` to preserve the
// `std::error::Error::source()` chain, and those sources aren't `Eq`.
// Match on the variant rather than comparing error values.
#[derive(Error, Debug)]
pub enum Error {
/// URL parse error.
#[error("Url parsing error.")]
BadUrl(String),
/// Token parse error.
#[error("Token parsing error.")]
BadToken,
/// Header parse error.
#[error("Headers parsing error.")]
BadHeader(String),
/// Missing the base URL of the REST API server.
#[error("Set an URL before calling build.")]
UnitializedUrl,
/// Missing the authentication token for the REST API server.
#[error("Set a token before calling build.")]
UnitializedToken,
/// HTTP calls errors
#[error("http error: {0}")]
Request(RequestError),
/// Tried to
/// [`build`](crate::atomic::AtomicGroupBuilder::build) a
/// transaction group with zero transactions. Equivalent of
/// [`algonaut_transaction::error::TransactionError::EmptyTransactionListError`]
/// at the top-level error layer.
#[error("transaction group is empty")]
EmptyTransactionGroup,
/// The transaction group is at maximum capacity (16 txns by protocol).
#[error("composer group full (max {max} transactions)")]
ComposerGroupFull { max: usize },
/// An ABI method call returned but the application did not emit a
/// matching `log` entry, so the return value cannot be decoded.
#[error("app call transaction did not log a return value")]
MissingReturnLog,
/// `algod` was asked to compile TEAL with a source-map but the response
/// did not carry one.
#[error("algod did not return a sourcemap")]
MissingSourcemap,
/// Algod reported that the transaction was kicked out of its pool
/// before being included in a block (expired `LastValid`, underfunded,
/// group invalid, etc.). The `reason` string is algod's `pool-error`
/// verbatim and is diagnostic only — match on the variant, not the
/// message. Note: this is a node-local view; a tx evicted from one
/// node's pool could in principle still be alive in peers' pools.
#[error("transaction pool error: {reason}")]
PendingTransactionPoolError { reason: String },
/// [`PendingSubmission::confirm_with`] (or the equivalent internal
/// helper on the atomic-transaction-composer) reached its deadline
/// without observing a confirmation.
///
/// [`PendingSubmission::confirm_with`]: crate::algod::v2::PendingSubmission::confirm_with
#[error("pending transaction timed out ({timeout:?})")]
PendingTransactionTimeout { timeout: Duration },
/// A [`Signer`](algonaut_transaction::Signer) returned output that does
/// not match the request the composer made: wrong count, wrong order,
/// or a signature wrapping a different transaction than the one asked
/// for. The signed group is rejected rather than submitted. `reason`
/// is diagnostic only.
#[error("signer returned invalid output: {reason}")]
SignerOutputInvalid { reason: String },
/// [`sign`](crate::atomic::UnsignedAtomicGroup::sign)
/// was called on a group whose slot at `index` has no signer
/// (`TransactionWithSigner::unsigned`). An unsigned slot cannot
/// produce a submittable signature; it is only valid for `simulate`.
/// Attach a signer, or simulate the group instead of signing it.
#[error("transaction at index {index} has no signer (only valid for simulate)")]
MissingSigner { index: usize },
/// An ABI [`MethodCall`](crate::atomic::MethodCall) was given a number
/// of arguments that does not match its method signature.
#[error("ABI method expected {expected} argument(s), got {actual}")]
AbiArgumentCountMismatch { expected: usize, actual: usize },
/// A transaction handed to the composer already carries a group id, so
/// it cannot be added to a new atomic group.
#[error("transaction already belongs to a group")]
TransactionAlreadyGrouped,
/// A transaction-typed ABI argument did not match the transaction type
/// the method signature requires. `expected`/`actual` are diagnostic
/// only — match on the variant.
#[error("expected transaction of type {expected}, got {actual}")]
TransactionTypeMismatch { expected: String, actual: String },
/// The method signature declared a transaction-typed argument, but the
/// supplied value was a plain ABI value rather than a transaction.
#[error("expected a transaction argument")]
ExpectedTransactionArgument,
/// An ABI argument could not be converted to the type the method
/// signature requires. `actual` is a diagnostic rendering of the value
/// that was supplied.
#[error("invalid ABI argument: expected {expected}, got {actual}")]
InvalidAbiArgument {
expected: &'static str,
actual: String,
},
/// A logged ABI return value was not valid base64 and could not be
/// decoded. The source carries the underlying decode failure.
#[error("failed to base64-decode a logged ABI return value")]
Base64DecodeError {
#[source]
source: data_encoding::DecodeError,
},
/// The composer reassembled signer output but found no signature for
/// the slot at `index`. This is an SDK-internal invariant violation
/// (please open an [issue](https://github.com/manuelmauro/algonaut/issues)!).
#[error("internal error: no signature produced for transaction at index {index}")]
InternalSigningIncomplete { index: usize },
/// A transaction construction or signing error from
/// [`algonaut_transaction`], preserved as the error source.
#[error(transparent)]
Transaction(#[from] TransactionError),
/// An ABI encode/decode error from [`algonaut_abi`], preserved as the
/// error source.
#[error(transparent)]
Abi(#[from] AbiError),
/// General text-only errors. Dedicated error variants can be created, if needed.
#[error("Msg: {0}")]
Msg(String),
/// Clearly SDK caused errors (please open an [issue](https://github.com/manuelmauro/algonaut/issues)!)
/// TODO rename in unexpected
#[error("Internal error: {0}")]
Internal(String),
}
impl Error {
/// Returns if the error is a `RequestError` that failed with a status code of 404.
pub fn is_404(&self) -> bool {
if let Some(e) = self.as_request_error() {
e.is_404()
} else {
false
}
}
/// Gets the details of a request error, or none otherwise.
fn as_request_error(&self) -> Option<&RequestError> {
match self {
Self::Request(e) => Some(e),
_ => None,
}
}
}
#[derive(Error, Clone, Debug, PartialEq, Eq)]
#[error("{:?}, {}", url, details)]
pub struct RequestError {
pub url: Option<String>,
pub details: RequestErrorDetails,
}
impl RequestError {
pub fn new(url: Option<String>, details: RequestErrorDetails) -> RequestError {
RequestError { url, details }
}
/// Returns if the cause of the error is a 404 response from the client.
fn is_404(&self) -> bool {
self.details.status() == Some(404)
}
}
#[derive(Error, Clone, Debug, PartialEq, Eq)]
pub enum RequestErrorDetails {
/// Http call error with optional message (returned by remote API)
#[error("Http error: {}, {}", status, message)]
Http { status: u16, message: String },
/// Timeout
#[error("Timeout connecting to the server.")]
Timeout,
/// Client generated errors (while e.g. building request or decoding response)
#[error("Client error: {}", description)]
Client { description: String },
}
impl RequestErrorDetails {
/// Gets the status code of the request.
///
/// Returns `None` if the request did not receive a response.
fn status(&self) -> Option<u16> {
match self {
Self::Http { status, .. } => Some(*status),
_ => None,
}
}
}
#[cfg(feature = "algod")]
impl From<crate::algod::v2::error::AlgodError> for Error {
fn from(error: crate::algod::v2::error::AlgodError) -> Self {
use crate::algod::v2::error::AlgodError;
match error {
AlgodError::Reqwest(e) => {
let details = if e.is_timeout() {
RequestErrorDetails::Timeout
} else {
RequestErrorDetails::Client {
description: e.to_string(),
}
};
Error::Request(RequestError::new(e.url().map(|u| u.to_string()), details))
}
AlgodError::Decode(e) => Error::Internal(format!("JSON decode: {e}")),
AlgodError::Msgpack(e) => Error::Internal(format!("msgpack decode: {e}")),
AlgodError::Io(e) => Error::Internal(format!("I/O: {e}")),
AlgodError::ResponseError { status, content } => Error::Request(RequestError::new(
None,
RequestErrorDetails::Http {
status,
message: content,
},
)),
}
}
}
#[cfg(feature = "indexer")]
impl From<crate::indexer::v2::error::IndexerError> for Error {
fn from(error: crate::indexer::v2::error::IndexerError) -> Self {
use crate::indexer::v2::error::IndexerError;
match error {
IndexerError::Reqwest(e) => {
let details = if e.is_timeout() {
RequestErrorDetails::Timeout
} else {
RequestErrorDetails::Client {
description: e.to_string(),
}
};
Error::Request(RequestError::new(e.url().map(|u| u.to_string()), details))
}
IndexerError::Decode(e) => Error::Internal(format!("JSON decode: {e}")),
IndexerError::Io(e) => Error::Internal(format!("I/O: {e}")),
IndexerError::ResponseError { status, content } => Error::Request(RequestError::new(
None,
RequestErrorDetails::Http {
status,
message: content,
},
)),
}
}
}
#[cfg(feature = "kmd")]
impl From<algonaut_kmd::error::ClientError> for Error {
fn from(error: algonaut_kmd::error::ClientError) -> Self {
match error {
algonaut_kmd::error::ClientError::BadUrl(msg) => Error::BadUrl(msg),
algonaut_kmd::error::ClientError::BadToken => Error::BadToken,
algonaut_kmd::error::ClientError::BadHeader(msg) => Error::BadHeader(msg),
algonaut_kmd::error::ClientError::Request(e) => Error::Request(e.into()),
algonaut_kmd::error::ClientError::Msg(msg) => Error::Msg(msg),
}
}
}
#[cfg(feature = "kmd")]
impl From<algonaut_kmd::error::RequestError> for RequestError {
fn from(error: algonaut_kmd::error::RequestError) -> Self {
RequestError::new(error.url.clone(), error.details.into())
}
}
#[cfg(feature = "kmd")]
impl From<algonaut_kmd::error::RequestErrorDetails> for RequestErrorDetails {
fn from(details: algonaut_kmd::error::RequestErrorDetails) -> Self {
match details {
algonaut_kmd::error::RequestErrorDetails::Http { status, message } => {
RequestErrorDetails::Http { status, message }
}
algonaut_kmd::error::RequestErrorDetails::Timeout => RequestErrorDetails::Timeout {},
algonaut_kmd::error::RequestErrorDetails::Client { description } => {
RequestErrorDetails::Client { description }
}
}
}
}
impl From<rmp_serde::encode::Error> for Error {
fn from(error: rmp_serde::encode::Error) -> Self {
Error::Internal(error.to_string())
}
}
impl From<String> for Error {
fn from(error: String) -> Self {
Error::Internal(error)
}
}
#[test]
fn check_404() {
let not_found_error = Error::Request(RequestError::new(
Some("testing".to_owned()),
RequestErrorDetails::Http {
status: 404,
message: "not found".to_owned(),
},
));
let bad_request_error = Error::Request(RequestError::new(
None,
RequestErrorDetails::Http {
status: 400,
message: "bad request".to_owned(),
},
));
let unrelated_error = Error::UnitializedToken;
assert!(
not_found_error.is_404(),
"a 404 request error is saying that it is not a 404 error"
);
assert!(
!bad_request_error.is_404(),
"a 400 request error is saying that it is a 404 error"
);
assert!(
!unrelated_error.is_404(),
"an unrelated request error is saying that it is a 404 error"
);
}