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, 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>, // Boxed to save space
43}
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(&parameters).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(&parameters)
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}