1use iota_sdk2::types::{
6 Address, CheckpointData, CheckpointDigest, CheckpointSequenceNumber, EpochId, Object, ObjectId,
7 SignedCheckpointSummary, SignedTransaction, StructTag, TransactionDigest, ValidatorCommittee,
8 Version,
9};
10use reqwest::{StatusCode, Url, header::HeaderValue};
11use tap::Pipe;
12
13use crate::{
14 ExecuteTransactionQueryParameters,
15 accounts::{AccountOwnedObjectInfo, ListAccountOwnedObjectsQueryParameters},
16 checkpoints::ListCheckpointsQueryParameters,
17 coins::CoinInfo,
18 health::Threshold,
19 info::NodeInfo,
20 objects::{DynamicFieldInfo, ListDynamicFieldsQueryParameters},
21 system::{
22 GasInfo, ProtocolConfigResponse, SystemStateSummary, X_IOTA_MAX_SUPPORTED_PROTOCOL_VERSION,
23 X_IOTA_MIN_SUPPORTED_PROTOCOL_VERSION,
24 },
25 transactions::{
26 ListTransactionsQueryParameters, TransactionExecutionResponse, TransactionResponse,
27 },
28 types::{
29 X_IOTA_CHAIN, X_IOTA_CHAIN_ID, X_IOTA_CHECKPOINT_HEIGHT, X_IOTA_CURSOR, X_IOTA_EPOCH,
30 X_IOTA_LOWEST_AVAILABLE_CHECKPOINT, X_IOTA_LOWEST_AVAILABLE_CHECKPOINT_OBJECTS,
31 X_IOTA_TIMESTAMP_MS,
32 },
33};
34
35static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
36
37#[derive(Clone, Debug)]
38pub struct Client {
39 inner: reqwest::Client,
40 url: Box<reqwest::Url>, }
42
43impl Client {
44 pub fn new(url: &str) -> Result<Self> {
45 let mut url = Url::parse(url).map_err(Error::from_error)?;
46
47 if url.cannot_be_a_base() {
48 return Err(Error::new_message(format!(
49 "provided url '{url}' cannot be used as a base"
50 )));
51 }
52
53 url.set_path("/api/v1/");
54
55 let inner = reqwest::ClientBuilder::new()
56 .user_agent(USER_AGENT)
57 .build()?;
58
59 Self {
60 inner,
61 url: Box::new(url),
62 }
63 .pipe(Ok)
64 }
65
66 pub(super) fn client(&self) -> &reqwest::Client {
67 &self.inner
68 }
69
70 pub fn url(&self) -> &Url {
71 &self.url
72 }
73
74 pub async fn node_info(&self) -> Result<Response<NodeInfo>> {
75 let url = self.url().join("")?;
76
77 let response = self
78 .inner
79 .get(url)
80 .header(reqwest::header::ACCEPT, crate::APPLICATION_JSON)
81 .send()
82 .await?;
83
84 self.json(response).await
85 }
86
87 pub async fn health_check(&self, threshold_seconds: Option<u32>) -> Result<Response<()>> {
88 let url = self.url().join("health")?;
89 let query = Threshold { threshold_seconds };
90
91 let response = self.inner.get(url).query(&query).send().await?;
92
93 self.empty(response).await
94 }
95
96 pub async fn get_coin_info(&self, coin_type: &StructTag) -> Result<Response<CoinInfo>> {
97 let url = self.url().join(&format!("coins/{coin_type}"))?;
98
99 let response = self
100 .inner
101 .get(url)
102 .header(reqwest::header::ACCEPT, crate::APPLICATION_JSON)
103 .send()
104 .await?;
105
106 self.json(response).await
107 }
108
109 pub async fn list_account_objects(
110 &self,
111 account: Address,
112 parameters: &ListAccountOwnedObjectsQueryParameters,
113 ) -> Result<Response<Vec<AccountOwnedObjectInfo>>> {
114 let url = self.url().join(&format!("account/{account}/objects"))?;
115
116 let response = self
117 .inner
118 .get(url)
119 .query(parameters)
120 .header(reqwest::header::ACCEPT, crate::APPLICATION_JSON)
121 .send()
122 .await?;
123
124 self.json(response).await
125 }
126
127 pub async fn get_object(&self, object_id: ObjectId) -> Result<Response<Object>> {
128 let url = self.url().join(&format!("objects/{object_id}"))?;
129
130 let response = self
131 .inner
132 .get(url)
133 .header(reqwest::header::ACCEPT, crate::APPLICATION_BCS)
134 .send()
135 .await?;
136
137 self.bcs(response).await
138 }
139
140 pub async fn get_object_with_version(
141 &self,
142 object_id: ObjectId,
143 version: Version,
144 ) -> Result<Response<Object>> {
145 let url = self
146 .url()
147 .join(&format!("objects/{object_id}/version/{version}"))?;
148
149 let response = self
150 .inner
151 .get(url)
152 .header(reqwest::header::ACCEPT, crate::APPLICATION_BCS)
153 .send()
154 .await?;
155
156 self.bcs(response).await
157 }
158
159 pub async fn list_dynamic_fields(
160 &self,
161 object_id: ObjectId,
162 parameters: &ListDynamicFieldsQueryParameters,
163 ) -> Result<Response<Vec<DynamicFieldInfo>>> {
164 let url = self.url().join(&format!("objects/{object_id}"))?;
165
166 let response = self
167 .inner
168 .get(url)
169 .query(parameters)
170 .header(reqwest::header::ACCEPT, crate::APPLICATION_JSON)
171 .send()
172 .await?;
173
174 self.json(response).await
175 }
176
177 pub async fn get_gas_info(&self) -> Result<Response<GasInfo>> {
178 let url = self.url().join("system/gas")?;
179
180 let response = self
181 .inner
182 .get(url)
183 .header(reqwest::header::ACCEPT, crate::APPLICATION_JSON)
184 .send()
185 .await?;
186
187 self.json(response).await
188 }
189
190 pub async fn get_reference_gas_price(&self) -> Result<u64> {
191 self.get_gas_info()
192 .await
193 .map(Response::into_inner)
194 .map(|info| info.reference_gas_price)
195 }
196
197 pub async fn get_current_protocol_config(&self) -> Result<Response<ProtocolConfigResponse>> {
198 let url = self.url().join("system/protocol")?;
199
200 let response = self
201 .inner
202 .get(url)
203 .header(reqwest::header::ACCEPT, crate::APPLICATION_JSON)
204 .send()
205 .await?;
206
207 self.json(response).await
208 }
209
210 pub async fn get_protocol_config(
211 &self,
212 version: u64,
213 ) -> Result<Response<ProtocolConfigResponse>> {
214 let url = self.url().join(&format!("system/protocol/{version}"))?;
215
216 let response = self
217 .inner
218 .get(url)
219 .header(reqwest::header::ACCEPT, crate::APPLICATION_JSON)
220 .send()
221 .await?;
222
223 self.json(response).await
224 }
225
226 pub async fn get_system_state_summary(&self) -> Result<Response<SystemStateSummary>> {
227 let url = self.url().join("system")?;
228
229 let response = self
230 .inner
231 .get(url)
232 .header(reqwest::header::ACCEPT, crate::APPLICATION_JSON)
233 .send()
234 .await?;
235
236 self.json(response).await
237 }
238
239 pub async fn get_current_committee(&self) -> Result<Response<ValidatorCommittee>> {
240 let url = self.url().join("system/committee")?;
241
242 let response = self
243 .inner
244 .get(url)
245 .header(reqwest::header::ACCEPT, crate::APPLICATION_BCS)
246 .send()
247 .await?;
248
249 self.bcs(response).await
250 }
251
252 pub async fn get_committee(&self, epoch: EpochId) -> Result<Response<ValidatorCommittee>> {
253 let url = self.url().join(&format!("system/committee/{epoch}"))?;
254
255 let response = self
256 .inner
257 .get(url)
258 .header(reqwest::header::ACCEPT, crate::APPLICATION_BCS)
259 .send()
260 .await?;
261
262 self.bcs(response).await
263 }
264
265 pub async fn get_checkpoint(
266 &self,
267 checkpoint_sequence_number: CheckpointSequenceNumber,
268 ) -> Result<Response<SignedCheckpointSummary>> {
269 let url = self
270 .url()
271 .join(&format!("checkpoints/{checkpoint_sequence_number}"))?;
272
273 let response = self
274 .inner
275 .get(url)
276 .header(reqwest::header::ACCEPT, crate::APPLICATION_BCS)
277 .send()
278 .await?;
279
280 self.bcs(response).await
281 }
282
283 pub async fn get_latest_checkpoint(&self) -> Result<Response<SignedCheckpointSummary>> {
284 let parameters = ListCheckpointsQueryParameters {
285 limit: Some(1),
286 start: None,
287 direction: None,
288 };
289
290 let (mut page, parts) = self.list_checkpoints(¶meters).await?.into_parts();
291
292 let checkpoint = page
293 .pop()
294 .ok_or_else(|| Error::new_message("server returned empty checkpoint list"))?;
295
296 Ok(Response::new(checkpoint, parts))
297 }
298
299 pub async fn list_checkpoints(
300 &self,
301 parameters: &ListCheckpointsQueryParameters,
302 ) -> Result<Response<Vec<SignedCheckpointSummary>>> {
303 let url = self.url().join("checkpoints")?;
304
305 let response = self
306 .inner
307 .get(url)
308 .query(parameters)
309 .header(reqwest::header::ACCEPT, crate::APPLICATION_BCS)
310 .send()
311 .await?;
312
313 self.bcs(response).await
314 }
315
316 pub async fn get_full_checkpoint(
317 &self,
318 checkpoint_sequence_number: CheckpointSequenceNumber,
319 ) -> Result<Response<CheckpointData>> {
320 let url = self
321 .url()
322 .join(&format!("checkpoints/{checkpoint_sequence_number}/full"))?;
323
324 let response = self
325 .inner
326 .get(url)
327 .header(reqwest::header::ACCEPT, crate::APPLICATION_BCS)
328 .send()
329 .await?;
330
331 self.bcs(response).await
332 }
333
334 pub async fn get_transaction(
335 &self,
336 transaction: &TransactionDigest,
337 ) -> Result<Response<TransactionResponse>> {
338 let url = self.url().join(&format!("transactions/{transaction}"))?;
339
340 let response = self
341 .inner
342 .get(url)
343 .header(reqwest::header::ACCEPT, crate::APPLICATION_BCS)
344 .send()
345 .await?;
346
347 self.bcs(response).await
348 }
349
350 pub async fn list_transactions(
351 &self,
352 parameters: &ListTransactionsQueryParameters,
353 ) -> Result<Response<Vec<TransactionResponse>>> {
354 let url = self.url().join("transactions")?;
355
356 let response = self
357 .inner
358 .get(url)
359 .query(parameters)
360 .header(reqwest::header::ACCEPT, crate::APPLICATION_BCS)
361 .send()
362 .await?;
363
364 self.bcs(response).await
365 }
366
367 pub async fn execute_transaction(
368 &self,
369 parameters: &ExecuteTransactionQueryParameters,
370 transaction: &SignedTransaction,
371 ) -> Result<Response<TransactionExecutionResponse>> {
372 let url = self.url().join("transactions")?;
373
374 let body = bcs::to_bytes(transaction)?;
375
376 let response = self
377 .inner
378 .post(url)
379 .query(parameters)
380 .header(reqwest::header::ACCEPT, crate::APPLICATION_BCS)
381 .header(reqwest::header::CONTENT_TYPE, crate::APPLICATION_BCS)
382 .body(body)
383 .send()
384 .await?;
385
386 self.bcs(response).await
387 }
388
389 pub async fn get_epoch_last_checkpoint(
390 &self,
391 epoch: EpochId,
392 ) -> Result<Response<SignedCheckpointSummary>> {
393 let url = self
394 .url()
395 .join(&format!("epochs/{epoch}/last-checkpoint"))?;
396
397 let response = self
398 .inner
399 .get(url)
400 .header(reqwest::header::ACCEPT, crate::APPLICATION_BCS)
401 .send()
402 .await?;
403
404 self.bcs(response).await
405 }
406
407 async fn check_response(
408 &self,
409 response: reqwest::Response,
410 ) -> Result<(reqwest::Response, ResponseParts)> {
411 let parts = ResponseParts::from_reqwest_response(&response);
412
413 if !response.status().is_success() {
414 let error = match response.text().await {
415 Ok(body) => Error::new_message(body),
416 Err(e) => Error::from_error(e),
417 }
418 .pipe(|e| e.with_parts(parts));
419
420 return Err(error);
421 }
422
423 Ok((response, parts))
424 }
425
426 async fn empty(&self, response: reqwest::Response) -> Result<Response<()>> {
427 let (_response, parts) = self.check_response(response).await?;
428 Ok(Response::new((), parts))
429 }
430
431 async fn json<T: serde::de::DeserializeOwned>(
432 &self,
433 response: reqwest::Response,
434 ) -> Result<Response<T>> {
435 let (response, parts) = self.check_response(response).await?;
436
437 let json = response.json().await?;
438 Ok(Response::new(json, parts))
439 }
440
441 pub(super) async fn bcs<T: serde::de::DeserializeOwned>(
442 &self,
443 response: reqwest::Response,
444 ) -> Result<Response<T>> {
445 let (response, parts) = self.check_response(response).await?;
446
447 let bytes = response.bytes().await?;
448 match bcs::from_bytes(&bytes) {
449 Ok(bcs) => Ok(Response::new(bcs, parts)),
450 Err(e) => Err(Error::from_error(e).with_parts(parts)),
451 }
452 }
453}
454
455#[derive(Debug)]
456pub struct ResponseParts {
457 pub status: StatusCode,
458 pub chain_id: Option<CheckpointDigest>,
459 pub chain: Option<String>,
460 pub epoch: Option<EpochId>,
461 pub checkpoint_height: Option<CheckpointSequenceNumber>,
462 pub timestamp_ms: Option<u64>,
463 pub lowest_available_checkpoint: Option<CheckpointSequenceNumber>,
464 pub lowest_available_checkpoint_objects: Option<CheckpointSequenceNumber>,
465 pub cursor: Option<String>,
466 pub min_supported_protocol_version: Option<u64>,
467 pub max_supported_protocol_version: Option<u64>,
468}
469
470impl ResponseParts {
471 fn from_reqwest_response(response: &reqwest::Response) -> Self {
472 let headers = response.headers();
473 let status = response.status();
474 let chain_id = headers
475 .get(X_IOTA_CHAIN_ID)
476 .map(HeaderValue::as_bytes)
477 .and_then(|s| CheckpointDigest::from_base58(s).ok());
478 let chain = headers
479 .get(X_IOTA_CHAIN)
480 .and_then(|h| h.to_str().ok())
481 .map(ToOwned::to_owned);
482 let epoch = headers
483 .get(X_IOTA_EPOCH)
484 .and_then(|h| h.to_str().ok())
485 .and_then(|s| s.parse().ok());
486 let checkpoint_height = headers
487 .get(X_IOTA_CHECKPOINT_HEIGHT)
488 .and_then(|h| h.to_str().ok())
489 .and_then(|s| s.parse().ok());
490 let timestamp_ms = headers
491 .get(X_IOTA_TIMESTAMP_MS)
492 .and_then(|h| h.to_str().ok())
493 .and_then(|s| s.parse().ok());
494 let lowest_available_checkpoint = headers
495 .get(X_IOTA_LOWEST_AVAILABLE_CHECKPOINT)
496 .and_then(|h| h.to_str().ok())
497 .and_then(|s| s.parse().ok());
498 let lowest_available_checkpoint_objects = headers
499 .get(X_IOTA_LOWEST_AVAILABLE_CHECKPOINT_OBJECTS)
500 .and_then(|h| h.to_str().ok())
501 .and_then(|s| s.parse().ok());
502 let cursor = headers
503 .get(X_IOTA_CURSOR)
504 .and_then(|h| h.to_str().ok())
505 .map(ToOwned::to_owned);
506 let min_supported_protocol_version = headers
507 .get(X_IOTA_MIN_SUPPORTED_PROTOCOL_VERSION)
508 .and_then(|h| h.to_str().ok())
509 .and_then(|s| s.parse().ok());
510 let max_supported_protocol_version = headers
511 .get(X_IOTA_MAX_SUPPORTED_PROTOCOL_VERSION)
512 .and_then(|h| h.to_str().ok())
513 .and_then(|s| s.parse().ok());
514
515 Self {
516 status,
517 chain_id,
518 chain,
519 epoch,
520 checkpoint_height,
521 timestamp_ms,
522 lowest_available_checkpoint,
523 lowest_available_checkpoint_objects,
524 cursor,
525 min_supported_protocol_version,
526 max_supported_protocol_version,
527 }
528 }
529}
530
531#[derive(Debug)]
532pub struct Response<T> {
533 inner: T,
534
535 parts: ResponseParts,
536}
537
538impl<T> Response<T> {
539 pub fn new(inner: T, parts: ResponseParts) -> Self {
540 Self { inner, parts }
541 }
542
543 pub fn inner(&self) -> &T {
544 &self.inner
545 }
546
547 pub fn into_inner(self) -> T {
548 self.inner
549 }
550
551 pub fn parts(&self) -> &ResponseParts {
552 &self.parts
553 }
554
555 pub fn into_parts(self) -> (T, ResponseParts) {
556 (self.inner, self.parts)
557 }
558
559 pub fn map<U, F>(self, f: F) -> Response<U>
560 where
561 F: FnOnce(T) -> U,
562 {
563 let (inner, state) = self.into_parts();
564 Response::new(f(inner), state)
565 }
566}
567
568pub type Result<T, E = Error> = std::result::Result<T, E>;
569
570type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
571
572#[derive(Debug)]
573pub struct Error {
574 inner: Box<InnerError>,
575}
576
577#[derive(Debug)]
578struct InnerError {
579 parts: Option<ResponseParts>,
580 message: Option<String>,
581 source: Option<BoxError>,
582}
583
584impl Error {
585 fn empty() -> Self {
586 Self {
587 inner: Box::new(InnerError {
588 parts: None,
589 message: None,
590 source: None,
591 }),
592 }
593 }
594
595 fn from_error<E: Into<BoxError>>(error: E) -> Self {
596 Self::empty().with_error(error.into())
597 }
598
599 fn new_message<M: Into<String>>(message: M) -> Self {
600 Self::empty().with_message(message.into())
601 }
602
603 fn with_parts(mut self, parts: ResponseParts) -> Self {
604 self.inner.parts.replace(parts);
605 self
606 }
607
608 fn with_message(mut self, message: String) -> Self {
609 self.inner.message.replace(message);
610 self
611 }
612
613 fn with_error(mut self, error: BoxError) -> Self {
614 self.inner.source.replace(error);
615 self
616 }
617
618 pub fn status(&self) -> Option<StatusCode> {
619 self.parts().map(|parts| parts.status)
620 }
621
622 pub fn parts(&self) -> Option<&ResponseParts> {
623 self.inner.parts.as_ref()
624 }
625
626 pub fn message(&self) -> Option<&str> {
627 self.inner.message.as_deref()
628 }
629}
630
631impl std::fmt::Display for Error {
632 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
633 write!(f, "Rest Client Error:")?;
634 if let Some(status) = self.status() {
635 write!(f, " {status}")?;
636 }
637
638 if let Some(message) = self.message() {
639 write!(f, " '{message}'")?;
640 }
641
642 if let Some(source) = &self.inner.source {
643 write!(f, " '{source}'")?;
644 }
645
646 Ok(())
647 }
648}
649
650impl std::error::Error for Error {
651 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
652 self.inner.source.as_deref().map(|e| e as _)
653 }
654}
655
656impl From<reqwest::Error> for Error {
657 fn from(error: reqwest::Error) -> Self {
658 Self::from_error(error)
659 }
660}
661
662impl From<bcs::Error> for Error {
663 fn from(error: bcs::Error) -> Self {
664 Self::from_error(error)
665 }
666}
667
668impl From<url::ParseError> for Error {
669 fn from(error: url::ParseError) -> Self {
670 Self::from_error(error)
671 }
672}
673
674impl From<iota_types::iota_sdk2_conversions::SdkTypeConversionError> for Error {
675 fn from(value: iota_types::iota_sdk2_conversions::SdkTypeConversionError) -> Self {
676 Self::from_error(value)
677 }
678}