iota_rest_api/client/
sdk.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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>, // Boxed to save space
41}
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(&parameters).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}