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