1pub mod apis;
82pub mod error;
83pub mod iota_client_config;
84pub mod json_rpc_error;
85pub mod wallet_context;
86
87use std::{
88 collections::{HashMap, VecDeque},
89 fmt::{Debug, Formatter},
90 marker::PhantomData,
91 pin::Pin,
92 str::FromStr,
93 sync::Arc,
94 task::Poll,
95 time::Duration,
96};
97
98use async_trait::async_trait;
99use base64::Engine;
100use futures::TryStreamExt;
101pub use iota_json as json;
102use iota_json_rpc_api::{
103 CLIENT_SDK_TYPE_HEADER, CLIENT_SDK_VERSION_HEADER, CLIENT_TARGET_API_VERSION_HEADER,
104};
105pub use iota_json_rpc_types as rpc_types;
106use iota_json_rpc_types::{
107 IotaObjectDataFilter, IotaObjectDataOptions, IotaObjectResponse, IotaObjectResponseQuery, Page,
108};
109use iota_sdk_types::{Address, ObjectId, StructTag};
110use iota_transaction_builder::{DataReader, TransactionBuilder};
111pub use iota_types as types;
112use jsonrpsee::{
113 core::client::ClientT,
114 http_client::{HeaderMap, HeaderValue, HttpClient, HttpClientBuilder},
115 rpc_params,
116 ws_client::{PingConfig, WsClient, WsClientBuilder},
117};
118use reqwest::header::HeaderName;
119use rustls::crypto::{CryptoProvider, ring};
120use serde_json::Value;
121
122use crate::{
123 apis::{CoinReadApi, EventApi, GovernanceApi, QuorumDriverApi, ReadApi},
124 error::{Error, IotaRpcResult},
125};
126
127pub const IOTA_COIN_TYPE: &str = "0x2::iota::IOTA";
128pub const IOTA_LOCAL_NETWORK_URL: &str = "http://127.0.0.1:9000";
129pub const IOTA_LOCAL_NETWORK_URL_0: &str = "http://0.0.0.0:9000";
130pub const IOTA_LOCAL_NETWORK_GRAPHQL_URL: &str = "http://127.0.0.1:9125";
131pub const IOTA_LOCAL_NETWORK_GRPC_URL: &str = "http://127.0.0.1:50051";
132pub const IOTA_LOCAL_NETWORK_GAS_URL: &str = "http://127.0.0.1:9123/v1/gas";
133pub const IOTA_DEVNET_URL: &str = "https://api.devnet.iota.cafe";
134pub const IOTA_DEVNET_GRAPHQL_URL: &str = "https://graphql.devnet.iota.cafe";
135pub const IOTA_DEVNET_GRPC_URL: &str = "https://grpc.devnet.iota.cafe:443";
136pub const IOTA_DEVNET_GAS_URL: &str = "https://faucet.devnet.iota.cafe/v1/gas";
137pub const IOTA_TESTNET_URL: &str = "https://api.testnet.iota.cafe";
138pub const IOTA_TESTNET_GRAPHQL_URL: &str = "https://graphql.testnet.iota.cafe";
139pub const IOTA_TESTNET_GRPC_URL: &str = "https://grpc.testnet.iota.cafe:443";
140pub const IOTA_TESTNET_GAS_URL: &str = "https://faucet.testnet.iota.cafe/v1/gas";
141pub const IOTA_MAINNET_URL: &str = "https://api.mainnet.iota.cafe";
142pub const IOTA_MAINNET_GRAPHQL_URL: &str = "https://graphql.mainnet.iota.cafe";
143pub const IOTA_MAINNET_GRPC_URL: &str = "https://grpc.mainnet.iota.cafe:443";
144
145pub struct IotaClientBuilder {
170 request_timeout: Duration,
171 max_concurrent_requests: Option<usize>,
172 ws_url: Option<String>,
173 ws_ping_interval: Option<Duration>,
174 basic_auth: Option<(String, String)>,
175 tls_config: Option<rustls::ClientConfig>,
176 headers: Option<HashMap<String, String>>,
177}
178
179impl Default for IotaClientBuilder {
180 fn default() -> Self {
181 Self {
182 request_timeout: Duration::from_secs(60),
183 max_concurrent_requests: None,
184 ws_url: None,
185 ws_ping_interval: None,
186 basic_auth: None,
187 tls_config: None,
188 headers: None,
189 }
190 }
191}
192
193impl IotaClientBuilder {
194 pub fn request_timeout(mut self, request_timeout: Duration) -> Self {
196 self.request_timeout = request_timeout;
197 self
198 }
199
200 pub fn max_concurrent_requests(mut self, max_concurrent_requests: usize) -> Self {
202 self.max_concurrent_requests = Some(max_concurrent_requests);
203 self
204 }
205
206 pub fn ws_url(mut self, url: impl AsRef<str>) -> Self {
208 self.ws_url = Some(url.as_ref().to_string());
209 self
210 }
211
212 pub fn ws_ping_interval(mut self, duration: Duration) -> Self {
214 self.ws_ping_interval = Some(duration);
215 self
216 }
217
218 pub fn basic_auth(mut self, username: impl AsRef<str>, password: impl AsRef<str>) -> Self {
220 self.basic_auth = Some((username.as_ref().to_string(), password.as_ref().to_string()));
221 self
222 }
223
224 pub fn custom_headers(mut self, headers: HashMap<String, String>) -> Self {
226 self.headers = Some(headers);
227 self
228 }
229
230 pub fn tls_config(mut self, config: rustls::ClientConfig) -> Self {
232 self.tls_config = Some(config);
233 self
234 }
235
236 pub async fn build(self, http: impl AsRef<str>) -> IotaRpcResult<IotaClient> {
255 if CryptoProvider::get_default().is_none() {
256 ring::default_provider().install_default().ok();
257 }
258
259 let client_version = env!("CARGO_PKG_VERSION");
260 let mut headers = HeaderMap::new();
261 headers.insert(
262 CLIENT_TARGET_API_VERSION_HEADER,
263 HeaderValue::from_static(client_version),
265 );
266 headers.insert(
267 CLIENT_SDK_VERSION_HEADER,
268 HeaderValue::from_static(client_version),
269 );
270 headers.insert(CLIENT_SDK_TYPE_HEADER, HeaderValue::from_static("rust"));
271
272 if let Some((username, password)) = self.basic_auth {
273 let auth =
274 base64::engine::general_purpose::STANDARD.encode(format!("{username}:{password}"));
275 headers.insert(
276 "authorization",
277 HeaderValue::from_str(&format!("Basic {auth}")).unwrap(),
279 );
280 }
281
282 if let Some(custom_headers) = self.headers {
283 for (key, value) in custom_headers {
284 let header_name =
285 HeaderName::from_str(&key).map_err(|e| Error::CustomHeaders(e.to_string()))?;
286 let header_value = HeaderValue::from_str(&value)
287 .map_err(|e| Error::CustomHeaders(e.to_string()))?;
288 headers.insert(header_name, header_value);
289 }
290 }
291
292 let ws = if let Some(url) = self.ws_url {
293 let mut builder = WsClientBuilder::default()
294 .max_request_size(2 << 30)
295 .set_headers(headers.clone())
296 .request_timeout(self.request_timeout);
297
298 if let Some(duration) = self.ws_ping_interval {
299 builder = builder.enable_ws_ping(PingConfig::new().ping_interval(duration))
300 }
301
302 if let Some(max_concurrent_requests) = self.max_concurrent_requests {
303 builder = builder.max_concurrent_requests(max_concurrent_requests);
304 }
305
306 builder.build(url).await.ok()
307 } else {
308 None
309 };
310
311 let mut http_builder = HttpClientBuilder::default()
312 .max_request_size(2 << 30)
313 .set_headers(headers)
314 .request_timeout(self.request_timeout);
315
316 if let Some(max_concurrent_requests) = self.max_concurrent_requests {
317 http_builder = http_builder.max_concurrent_requests(max_concurrent_requests);
318 }
319
320 if let Some(tls_config) = self.tls_config {
321 http_builder = http_builder.with_custom_cert_store(tls_config);
322 }
323
324 let http = http_builder.build(http)?;
325
326 let info = Self::get_server_info(&http, &ws).await?;
327
328 let rpc = RpcClient { http, ws, info };
329 let api = Arc::new(rpc);
330 let read_api = Arc::new(ReadApi::new(api.clone()));
331 let quorum_driver_api = QuorumDriverApi::new(api.clone());
332 let event_api = EventApi::new(api.clone());
333 let transaction_builder = TransactionBuilder::new(read_api.clone());
334 let coin_read_api = CoinReadApi::new(api.clone());
335 let governance_api = GovernanceApi::new(api.clone());
336
337 Ok(IotaClient {
338 api,
339 transaction_builder,
340 read_api,
341 coin_read_api,
342 event_api,
343 quorum_driver_api,
344 governance_api,
345 })
346 }
347
348 pub async fn build_localnet(self) -> IotaRpcResult<IotaClient> {
368 self.build(IOTA_LOCAL_NETWORK_URL).await
369 }
370
371 pub async fn build_devnet(self) -> IotaRpcResult<IotaClient> {
390 self.build(IOTA_DEVNET_URL).await
391 }
392
393 pub async fn build_testnet(self) -> IotaRpcResult<IotaClient> {
412 self.build(IOTA_TESTNET_URL).await
413 }
414
415 pub async fn build_mainnet(self) -> IotaRpcResult<IotaClient> {
434 self.build(IOTA_MAINNET_URL).await
435 }
436
437 async fn get_server_info(
441 http: &HttpClient,
442 ws: &Option<WsClient>,
443 ) -> Result<ServerInfo, Error> {
444 let rpc_spec: Value = http.request("rpc.discover", rpc_params![]).await?;
445 let version = rpc_spec
446 .pointer("/info/version")
447 .and_then(|v| v.as_str())
448 .ok_or_else(|| {
449 Error::Data("Fail parsing server version from rpc.discover endpoint.".into())
450 })?;
451 let rpc_methods = Self::parse_methods(&rpc_spec)?;
452
453 let subscriptions = if let Some(ws) = ws {
454 match ws.request("rpc.discover", rpc_params![]).await {
455 Ok(rpc_spec) => Self::parse_methods(&rpc_spec)?,
456 Err(_) => Vec::new(),
457 }
458 } else {
459 Vec::new()
460 };
461 let iota_system_state_v2_support =
462 rpc_methods.contains(&"iotax_getLatestIotaSystemStateV2".to_string());
463 Ok(ServerInfo {
464 rpc_methods,
465 subscriptions,
466 version: version.to_string(),
467 iota_system_state_v2_support,
468 })
469 }
470
471 fn parse_methods(server_spec: &Value) -> Result<Vec<String>, Error> {
472 let methods = server_spec
473 .pointer("/methods")
474 .and_then(|methods| methods.as_array())
475 .ok_or_else(|| {
476 Error::Data("Fail parsing server information from rpc.discover endpoint.".into())
477 })?;
478
479 Ok(methods
480 .iter()
481 .flat_map(|method| method["name"].as_str())
482 .map(|s| s.into())
483 .collect())
484 }
485}
486
487#[derive(Clone)]
524pub struct IotaClient {
525 api: Arc<RpcClient>,
526 transaction_builder: TransactionBuilder,
527 read_api: Arc<ReadApi>,
528 coin_read_api: CoinReadApi,
529 event_api: EventApi,
530 quorum_driver_api: QuorumDriverApi,
531 governance_api: GovernanceApi,
532}
533
534pub(crate) struct RpcClient {
535 http: HttpClient,
536 ws: Option<WsClient>,
537 info: ServerInfo,
538}
539
540impl Debug for RpcClient {
541 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
542 write!(
543 f,
544 "RPC client. Http: {:?}, Websocket: {:?}",
545 self.http, self.ws
546 )
547 }
548}
549
550struct ServerInfo {
553 rpc_methods: Vec<String>,
554 subscriptions: Vec<String>,
555 version: String,
556 iota_system_state_v2_support: bool,
557}
558
559impl IotaClient {
560 pub fn available_rpc_methods(&self) -> &Vec<String> {
563 &self.api.info.rpc_methods
564 }
565
566 pub fn available_subscriptions(&self) -> &Vec<String> {
569 &self.api.info.subscriptions
570 }
571
572 pub fn api_version(&self) -> &str {
578 &self.api.info.version
579 }
580
581 pub fn check_api_version(&self) -> IotaRpcResult<()> {
584 let server_version = self.api_version();
585 let client_version = env!("CARGO_PKG_VERSION");
586 if server_version != client_version {
587 return Err(Error::ServerVersionMismatch {
588 client_version: client_version.to_string(),
589 server_version: server_version.to_string(),
590 });
591 };
592 Ok(())
593 }
594
595 pub fn coin_read_api(&self) -> &CoinReadApi {
597 &self.coin_read_api
598 }
599
600 pub fn event_api(&self) -> &EventApi {
602 &self.event_api
603 }
604
605 pub fn governance_api(&self) -> &GovernanceApi {
607 &self.governance_api
608 }
609
610 pub fn quorum_driver_api(&self) -> &QuorumDriverApi {
612 &self.quorum_driver_api
613 }
614
615 pub fn read_api(&self) -> &ReadApi {
617 &self.read_api
618 }
619
620 pub fn transaction_builder(&self) -> &TransactionBuilder {
622 &self.transaction_builder
623 }
624
625 pub fn http(&self) -> &HttpClient {
627 &self.api.http
628 }
629
630 pub fn ws(&self) -> Option<&WsClient> {
632 self.api.ws.as_ref()
633 }
634}
635
636#[async_trait]
637impl DataReader for ReadApi {
638 async fn get_owned_objects(
639 &self,
640 address: Address,
641 object_type: StructTag,
642 cursor: Option<ObjectId>,
643 limit: Option<usize>,
644 options: IotaObjectDataOptions,
645 ) -> Result<iota_json_rpc_types::ObjectsPage, anyhow::Error> {
646 let query = Some(IotaObjectResponseQuery {
647 filter: Some(IotaObjectDataFilter::StructType(object_type)),
648 options: Some(options),
649 });
650
651 Ok(self
652 .get_owned_objects(address, query, cursor, limit)
653 .await?)
654 }
655
656 async fn get_object_with_options(
657 &self,
658 object_id: ObjectId,
659 options: IotaObjectDataOptions,
660 ) -> Result<IotaObjectResponse, anyhow::Error> {
661 Ok(self.get_object_with_options(object_id, options).await?)
662 }
663
664 async fn get_reference_gas_price(&self) -> Result<u64, anyhow::Error> {
666 Ok(self.get_reference_gas_price().await?)
667 }
668}
669
670pub trait PagedFn<O, C, F, E>: Sized + Fn(Option<C>) -> F
673where
674 O: Send,
675 C: Send,
676 F: futures::Future<Output = Result<Page<O, C>, E>> + Send,
677{
678 fn collect<T>(self) -> impl futures::Future<Output = Result<T, E>>
680 where
681 T: Default + Extend<O>,
682 {
683 self.stream().try_collect::<T>()
684 }
685
686 fn stream(self) -> PagedStream<O, C, F, E, Self> {
688 PagedStream::new(self)
689 }
690}
691
692impl<O, C, F, E, Fun> PagedFn<O, C, F, E> for Fun
693where
694 Fun: Fn(Option<C>) -> F,
695 O: Send,
696 C: Send,
697 F: futures::Future<Output = Result<Page<O, C>, E>> + Send,
698{
699}
700
701pub struct PagedStream<O, C, F, E, Fun> {
704 fun: Fun,
705 fut: Pin<Box<F>>,
706 next: VecDeque<O>,
707 has_next_page: bool,
708 _data: PhantomData<(E, C)>,
709}
710
711impl<O, C, F, E, Fun> PagedStream<O, C, F, E, Fun>
712where
713 Fun: Fn(Option<C>) -> F,
714{
715 pub fn new(fun: Fun) -> Self {
716 let fut = fun(None);
717 Self {
718 fun,
719 fut: Box::pin(fut),
720 next: Default::default(),
721 has_next_page: true,
722 _data: PhantomData,
723 }
724 }
725}
726
727impl<O, C, F, E, Fun> futures::Stream for PagedStream<O, C, F, E, Fun>
728where
729 O: Send,
730 C: Send,
731 F: futures::Future<Output = Result<Page<O, C>, E>> + Send,
732 Fun: Fn(Option<C>) -> F,
733{
734 type Item = Result<O, E>;
735
736 fn poll_next(
737 self: std::pin::Pin<&mut Self>,
738 cx: &mut std::task::Context<'_>,
739 ) -> Poll<Option<Self::Item>> {
740 let this = unsafe { self.get_unchecked_mut() };
741 if this.next.is_empty() && this.has_next_page {
742 match this.fut.as_mut().poll(cx) {
743 Poll::Ready(res) => match res {
744 Ok(mut page) => {
745 this.next.extend(page.data);
746 this.has_next_page = page.has_next_page;
747 if this.has_next_page {
748 this.fut.set((this.fun)(page.next_cursor.take()));
749 }
750 }
751 Err(e) => {
752 this.has_next_page = false;
753 return Poll::Ready(Some(Err(e)));
754 }
755 },
756 Poll::Pending => return Poll::Pending,
757 }
758 }
759 Poll::Ready(this.next.pop_front().map(Ok))
760 }
761}
762
763#[cfg(test)]
764mod test {
765 use futures::StreamExt;
766 use iota_json_rpc_types::Page;
767
768 use super::*;
769
770 #[tokio::test]
771 async fn test_get_all_pages() {
772 let data = (0..10000).collect::<Vec<_>>();
773 struct Endpoint {
774 data: Vec<i32>,
775 }
776
777 impl Endpoint {
778 async fn get_page(&self, cursor: Option<usize>) -> anyhow::Result<Page<i32, usize>> {
779 const PAGE_SIZE: usize = 100;
780 anyhow::ensure!(cursor.is_none_or(|v| v < self.data.len()), "invalid cursor");
781 let index = cursor.unwrap_or_default();
782 let data = self.data[index..]
783 .iter()
784 .copied()
785 .take(PAGE_SIZE)
786 .collect::<Vec<_>>();
787 let has_next_page = self.data.len() > index + PAGE_SIZE;
788 Ok(Page {
789 data,
790 next_cursor: has_next_page.then_some(index + PAGE_SIZE),
791 has_next_page,
792 })
793 }
794 }
795
796 let endpoint = Endpoint { data };
797
798 let mut stream = PagedFn::stream(async |cursor| endpoint.get_page(cursor).await);
799
800 assert_eq!(
801 stream
802 .by_ref()
803 .take(9999)
804 .try_collect::<Vec<_>>()
805 .await
806 .unwrap(),
807 endpoint.data[..9999]
808 );
809 assert_eq!(stream.by_ref().try_next().await.unwrap(), Some(9999));
810 assert!(stream.try_next().await.unwrap().is_none());
811
812 let mut bad_stream = PagedFn::stream(async |_| endpoint.get_page(Some(99999)).await);
813
814 assert!(bad_stream.try_next().await.is_err());
815 }
816}