sui_sdk/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! The Sui Rust SDK
5//!
6//! It aims at providing a similar SDK functionality like the one existing for
7//! [TypeScript](https://github.com/MystenLabs/sui/tree/main/sdk/typescript/).
8//! Sui Rust SDK builds on top of the [JSON RPC API](https://docs.sui.io/sui-jsonrpc)
9//! and therefore many of the return types are the ones specified in [sui_types].
10//!
11//! The API is split in several parts corresponding to different functionalities
12//! as following:
13//! * [CoinReadApi] - provides read-only functions to work with the coins
14//! * [EventApi] - provides event related functions functions to
15//! * [GovernanceApi] - provides functionality related to staking
16//! * [QuorumDriverApi] - provides functionality to execute a transaction
17//!   block and submit it to the fullnode(s)
18//! * [ReadApi] - provides functions for retrieving data about different
19//!   objects and transactions
20//! * <a href="../sui_transaction_builder/struct.TransactionBuilder.html" title="struct sui_transaction_builder::TransactionBuilder">TransactionBuilder</a> - provides functions for building transactions
21//!
22//! # Usage
23//! The main way to interact with the API is through the [SuiClientBuilder],
24//! which returns a [SuiClient] object from which the user can access the
25//! various APIs.
26//!
27//! ## Getting Started
28//! Add the Rust SDK to the project by running `cargo add sui-sdk` in the root
29//! folder of your Rust project.
30//!
31//! The main building block for the Sui Rust SDK is the [SuiClientBuilder],
32//! which provides a simple and straightforward way of connecting to a Sui
33//! network and having access to the different available APIs.
34//!
35//! A simple example that connects to a running Sui local network,
36//! the Sui devnet, and the Sui testnet is shown below.
37//! To successfully run this program, make sure to spin up a local
38//! network with a local validator, a fullnode, and a faucet server
39//! (see [here](https://github.com/stefan-mysten/sui/tree/rust_sdk_api_examples/crates/sui-sdk/examples#preqrequisites) for more information).
40//!
41//! ```rust,no_run
42//! use sui_sdk::SuiClientBuilder;
43//!
44//! #[tokio::main]
45//! async fn main() -> Result<(), anyhow::Error> {
46//!
47//!     let sui = SuiClientBuilder::default()
48//!         .build("http://127.0.0.1:9000") // provide the Sui network URL
49//!         .await?;
50//!     println!("Sui local network version: {:?}", sui.api_version());
51//!
52//!     // local Sui network, same result as above except using the dedicated function
53//!     let sui_local = SuiClientBuilder::default().build_localnet().await?;
54//!     println!("Sui local network version: {:?}", sui_local.api_version());
55//!
56//!     // Sui devnet running at `https://fullnode.devnet.io:443`
57//!     let sui_devnet = SuiClientBuilder::default().build_devnet().await?;
58//!     println!("Sui devnet version: {:?}", sui_devnet.api_version());
59//!
60//!     // Sui testnet running at `https://testnet.devnet.io:443`
61//!     let sui_testnet = SuiClientBuilder::default().build_testnet().await?;
62//!     println!("Sui testnet version: {:?}", sui_testnet.api_version());
63//!     Ok(())
64//!
65//! }
66//! ```
67//!
68//! ## Examples
69//!
70//! For detailed examples, please check the APIs docs and the examples folder
71//! in the [main repository](https://github.com/MystenLabs/sui/tree/main/crates/sui-sdk/examples).
72
73use std::collections::HashMap;
74use std::fmt::Debug;
75use std::fmt::Formatter;
76use std::str::FromStr;
77use std::sync::Arc;
78use std::time::Duration;
79
80use async_trait::async_trait;
81use base64::Engine;
82use jsonrpsee::core::client::ClientT;
83use jsonrpsee::http_client::{HeaderMap, HeaderValue, HttpClient, HttpClientBuilder};
84use jsonrpsee::rpc_params;
85use jsonrpsee::ws_client::{WsClient, WsClientBuilder};
86use reqwest::header::HeaderName;
87use serde_json::Value;
88
89use move_core_types::language_storage::StructTag;
90pub use sui_json as json;
91use sui_json_rpc_api::{
92    CLIENT_SDK_TYPE_HEADER, CLIENT_SDK_VERSION_HEADER, CLIENT_TARGET_API_VERSION_HEADER,
93};
94pub use sui_json_rpc_types as rpc_types;
95use sui_json_rpc_types::{
96    ObjectsPage, SuiObjectDataFilter, SuiObjectDataOptions, SuiObjectResponse,
97    SuiObjectResponseQuery,
98};
99use sui_transaction_builder::{DataReader, TransactionBuilder};
100pub use sui_types as types;
101use sui_types::base_types::{ObjectID, ObjectInfo, SuiAddress};
102
103use crate::apis::{CoinReadApi, EventApi, GovernanceApi, QuorumDriverApi, ReadApi};
104use crate::error::{Error, SuiRpcResult};
105
106pub mod apis;
107pub mod error;
108pub mod json_rpc_error;
109pub mod sui_client_config;
110pub mod verify_personal_message_signature;
111pub mod wallet_context;
112
113pub const SUI_COIN_TYPE: &str = "0x2::sui::SUI";
114pub const SUI_LOCAL_NETWORK_URL: &str = "http://127.0.0.1:9000";
115pub const SUI_LOCAL_NETWORK_URL_0: &str = "http://0.0.0.0:9000";
116pub const SUI_LOCAL_NETWORK_GAS_URL: &str = "http://127.0.0.1:5003/v2/gas";
117pub const SUI_DEVNET_URL: &str = "https://fullnode.devnet.sui.io:443";
118pub const SUI_TESTNET_URL: &str = "https://fullnode.testnet.sui.io:443";
119pub const SUI_MAINNET_URL: &str = "https://fullnode.mainnet.sui.io:443";
120
121/// A Sui client builder for connecting to the Sui network
122///
123/// By default the `maximum concurrent requests` is set to 256 and
124/// the `request timeout` is set to 60 seconds. These can be adjusted using the
125/// `max_concurrent_requests` function, and the `request_timeout` function.
126/// If you use the WebSocket, consider setting the `ws_ping_interval` field to a
127/// value of your choice to prevent the inactive WS subscription being
128/// disconnected due to proxy timeout.
129///
130/// # Examples
131///
132/// ```rust,no_run
133/// use sui_sdk::SuiClientBuilder;
134/// #[tokio::main]
135/// async fn main() -> Result<(), anyhow::Error> {
136///     let sui = SuiClientBuilder::default()
137///         .build("http://127.0.0.1:9000")
138///         .await?;
139///
140///     println!("Sui local network version: {:?}", sui.api_version());
141///     Ok(())
142/// }
143/// ```
144pub struct SuiClientBuilder {
145    request_timeout: Duration,
146    max_concurrent_requests: Option<usize>,
147    ws_url: Option<String>,
148    ws_ping_interval: Option<Duration>,
149    basic_auth: Option<(String, String)>,
150    headers: Option<HashMap<String, String>>,
151}
152
153impl Default for SuiClientBuilder {
154    fn default() -> Self {
155        Self {
156            request_timeout: Duration::from_secs(60),
157            max_concurrent_requests: None,
158            ws_url: None,
159            ws_ping_interval: None,
160            basic_auth: None,
161            headers: None,
162        }
163    }
164}
165
166impl SuiClientBuilder {
167    /// Set the request timeout to the specified duration
168    pub fn request_timeout(mut self, request_timeout: Duration) -> Self {
169        self.request_timeout = request_timeout;
170        self
171    }
172
173    /// Set the max concurrent requests allowed
174    pub fn max_concurrent_requests(mut self, max_concurrent_requests: usize) -> Self {
175        self.max_concurrent_requests = Some(max_concurrent_requests);
176        self
177    }
178
179    /// Set the WebSocket URL for the Sui network
180    pub fn ws_url(mut self, url: impl AsRef<str>) -> Self {
181        self.ws_url = Some(url.as_ref().to_string());
182        self
183    }
184
185    /// Set the WebSocket ping interval
186    pub fn ws_ping_interval(mut self, duration: Duration) -> Self {
187        self.ws_ping_interval = Some(duration);
188        self
189    }
190
191    /// Set the basic auth credentials for the HTTP client
192    pub fn basic_auth(mut self, username: impl AsRef<str>, password: impl AsRef<str>) -> Self {
193        self.basic_auth = Some((username.as_ref().to_string(), password.as_ref().to_string()));
194        self
195    }
196
197    /// Set custom headers for the HTTP client
198    pub fn custom_headers(mut self, headers: HashMap<String, String>) -> Self {
199        self.headers = Some(headers);
200        self
201    }
202
203    /// Returns a [SuiClient] object connected to the Sui network running at the URI provided.
204    ///
205    /// # Examples
206    ///
207    /// ```rust,no_run
208    /// use sui_sdk::SuiClientBuilder;
209    ///
210    /// #[tokio::main]
211    /// async fn main() -> Result<(), anyhow::Error> {
212    ///     let sui = SuiClientBuilder::default()
213    ///         .build("http://127.0.0.1:9000")
214    ///         .await?;
215    ///
216    ///     println!("Sui local version: {:?}", sui.api_version());
217    ///     Ok(())
218    /// }
219    /// ```
220    pub async fn build(self, http: impl AsRef<str>) -> SuiRpcResult<SuiClient> {
221        let client_version = env!("CARGO_PKG_VERSION");
222        let mut headers = HeaderMap::new();
223        headers.insert(
224            CLIENT_TARGET_API_VERSION_HEADER,
225            // in rust, the client version is the same as the target api version
226            HeaderValue::from_static(client_version),
227        );
228        headers.insert(
229            CLIENT_SDK_VERSION_HEADER,
230            HeaderValue::from_static(client_version),
231        );
232        headers.insert(CLIENT_SDK_TYPE_HEADER, HeaderValue::from_static("rust"));
233
234        if let Some((username, password)) = self.basic_auth {
235            let auth = base64::engine::general_purpose::STANDARD
236                .encode(format!("{}:{}", username, password));
237            headers.insert(
238                "authorization",
239                // reqwest::header::AUTHORIZATION,
240                HeaderValue::from_str(&format!("Basic {}", auth)).unwrap(),
241            );
242        }
243
244        if let Some(custom_headers) = self.headers {
245            for (key, value) in custom_headers {
246                let header_name = HeaderName::from_str(&key)
247                    .map_err(|e| Error::CustomHeadersError(e.to_string()))?;
248
249                let header_value = HeaderValue::from_str(&value)
250                    .map_err(|e| Error::CustomHeadersError(e.to_string()))?;
251                headers.insert(header_name, header_value);
252            }
253        }
254
255        let ws = if let Some(url) = self.ws_url {
256            let mut builder = WsClientBuilder::default()
257                .max_request_size(2 << 30)
258                .set_headers(headers.clone())
259                .request_timeout(self.request_timeout);
260
261            if let Some(duration) = self.ws_ping_interval {
262                builder = builder.enable_ws_ping(
263                    jsonrpsee::ws_client::PingConfig::new().ping_interval(duration),
264                );
265            }
266
267            if let Some(max_concurrent_requests) = self.max_concurrent_requests {
268                builder = builder.max_concurrent_requests(max_concurrent_requests);
269            }
270
271            builder.build(url).await.ok()
272        } else {
273            None
274        };
275
276        let mut http_builder = HttpClientBuilder::default()
277            .max_request_size(2 << 30)
278            .set_headers(headers.clone())
279            .request_timeout(self.request_timeout);
280
281        if let Some(max_concurrent_requests) = self.max_concurrent_requests {
282            http_builder = http_builder.max_concurrent_requests(max_concurrent_requests);
283        }
284
285        let http = http_builder.build(http)?;
286
287        let info = Self::get_server_info(&http, &ws).await?;
288
289        let rpc = RpcClient { http, ws, info };
290        let api = Arc::new(rpc);
291        let read_api = Arc::new(ReadApi::new(api.clone()));
292        let quorum_driver_api = QuorumDriverApi::new(api.clone());
293        let event_api = EventApi::new(api.clone());
294        let transaction_builder = TransactionBuilder::new(read_api.clone());
295        let coin_read_api = CoinReadApi::new(api.clone());
296        let governance_api = GovernanceApi::new(api.clone());
297
298        Ok(SuiClient {
299            api,
300            transaction_builder,
301            read_api,
302            coin_read_api,
303            event_api,
304            quorum_driver_api,
305            governance_api,
306        })
307    }
308
309    /// Returns a [SuiClient] object that is ready to interact with the local
310    /// development network (by default it expects the Sui network to be
311    /// up and running at `127.0.0.1:9000`).
312    ///
313    /// For connecting to a custom URI, use the `build` function instead.
314    ///
315    /// # Examples
316    ///
317    /// ```rust,no_run
318    /// use sui_sdk::SuiClientBuilder;
319    ///
320    /// #[tokio::main]
321    /// async fn main() -> Result<(), anyhow::Error> {
322    ///     let sui = SuiClientBuilder::default()
323    ///         .build_localnet()
324    ///         .await?;
325    ///
326    ///     println!("Sui local version: {:?}", sui.api_version());
327    ///     Ok(())
328    /// }
329    /// ```
330    pub async fn build_localnet(self) -> SuiRpcResult<SuiClient> {
331        self.build(SUI_LOCAL_NETWORK_URL).await
332    }
333
334    /// Returns a [SuiClient] object that is ready to interact with the Sui devnet.
335    ///
336    /// For connecting to a custom URI, use the `build` function instead..
337    ///
338    /// # Examples
339    ///
340    /// ```rust,no_run
341    /// use sui_sdk::SuiClientBuilder;
342    ///
343    /// #[tokio::main]
344    /// async fn main() -> Result<(), anyhow::Error> {
345    ///     let sui = SuiClientBuilder::default()
346    ///         .build_devnet()
347    ///         .await?;
348    ///
349    ///     println!("{:?}", sui.api_version());
350    ///     Ok(())
351    /// }
352    /// ```
353    pub async fn build_devnet(self) -> SuiRpcResult<SuiClient> {
354        self.build(SUI_DEVNET_URL).await
355    }
356
357    /// Returns a [SuiClient] object that is ready to interact with the Sui testnet.
358    ///
359    /// For connecting to a custom URI, use the `build` function instead.
360    ///
361    /// # Examples
362    ///
363    /// ```rust,no_run
364    /// use sui_sdk::SuiClientBuilder;
365    ///
366    /// #[tokio::main]
367    /// async fn main() -> Result<(), anyhow::Error> {
368    ///     let sui = SuiClientBuilder::default()
369    ///         .build_testnet()
370    ///         .await?;
371    ///
372    ///     println!("{:?}", sui.api_version());
373    ///     Ok(())
374    /// }
375    /// ```
376    pub async fn build_testnet(self) -> SuiRpcResult<SuiClient> {
377        self.build(SUI_TESTNET_URL).await
378    }
379
380    /// Returns a [SuiClient] object that is ready to interact with the Sui mainnet.
381    ///
382    /// For connecting to a custom URI, use the `build` function instead.
383    ///
384    /// # Examples
385    ///
386    /// ```rust,no_run
387    /// use sui_sdk::SuiClientBuilder;
388    ///
389    /// #[tokio::main]
390    /// async fn main() -> Result<(), anyhow::Error> {
391    ///     let sui = SuiClientBuilder::default()
392    ///         .build_mainnet()
393    ///         .await?;
394    ///
395    ///     println!("{:?}", sui.api_version());
396    ///     Ok(())
397    /// }
398    /// ```
399    pub async fn build_mainnet(self) -> SuiRpcResult<SuiClient> {
400        self.build(SUI_MAINNET_URL).await
401    }
402
403    /// Return the server information as a `ServerInfo` structure.
404    ///
405    /// Fails with an error if it cannot call the RPC discover.
406    async fn get_server_info(
407        http: &HttpClient,
408        ws: &Option<WsClient>,
409    ) -> Result<ServerInfo, Error> {
410        let rpc_spec: Value = http.request("rpc.discover", rpc_params![]).await?;
411        let version = rpc_spec
412            .pointer("/info/version")
413            .and_then(|v| v.as_str())
414            .ok_or_else(|| {
415                Error::DataError("Fail parsing server version from rpc.discover endpoint.".into())
416            })?;
417        let rpc_methods = Self::parse_methods(&rpc_spec)?;
418
419        let subscriptions = if let Some(ws) = ws {
420            match ws.request("rpc.discover", rpc_params![]).await {
421                Ok(rpc_spec) => Self::parse_methods(&rpc_spec)?,
422                Err(_) => Vec::new(),
423            }
424        } else {
425            Vec::new()
426        };
427        Ok(ServerInfo {
428            rpc_methods,
429            subscriptions,
430            version: version.to_string(),
431        })
432    }
433
434    fn parse_methods(server_spec: &Value) -> Result<Vec<String>, Error> {
435        let methods = server_spec
436            .pointer("/methods")
437            .and_then(|methods| methods.as_array())
438            .ok_or_else(|| {
439                Error::DataError(
440                    "Fail parsing server information from rpc.discover endpoint.".into(),
441                )
442            })?;
443
444        Ok(methods
445            .iter()
446            .flat_map(|method| method["name"].as_str())
447            .map(|s| s.into())
448            .collect())
449    }
450}
451
452/// SuiClient is the basic type that provides all the necessary abstractions for interacting with the Sui network.
453///
454/// # Usage
455///
456/// Use [SuiClientBuilder] to build a [SuiClient].
457///
458/// # Examples
459///
460/// ```rust,no_run
461/// use sui_sdk::types::base_types::SuiAddress;
462/// use sui_sdk::SuiClientBuilder;
463/// use std::str::FromStr;
464///
465/// #[tokio::main]
466/// async fn main() -> Result<(), anyhow::Error> {
467///     let sui = SuiClientBuilder::default()
468///      .build("http://127.0.0.1:9000")
469///      .await?;
470///
471///     println!("{:?}", sui.available_rpc_methods());
472///     println!("{:?}", sui.available_subscriptions());
473///     println!("{:?}", sui.api_version());
474///
475///     let address = SuiAddress::from_str("0x0000....0000")?;
476///     let owned_objects = sui
477///        .read_api()
478///        .get_owned_objects(address, None, None, None)
479///        .await?;
480///
481///     println!("{:?}", owned_objects);
482///
483///     Ok(())
484/// }
485/// ```
486#[derive(Clone)]
487pub struct SuiClient {
488    api: Arc<RpcClient>,
489    transaction_builder: TransactionBuilder,
490    read_api: Arc<ReadApi>,
491    coin_read_api: CoinReadApi,
492    event_api: EventApi,
493    quorum_driver_api: QuorumDriverApi,
494    governance_api: GovernanceApi,
495}
496
497pub(crate) struct RpcClient {
498    http: HttpClient,
499    ws: Option<WsClient>,
500    info: ServerInfo,
501}
502
503impl Debug for RpcClient {
504    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
505        write!(
506            f,
507            "RPC client. Http: {:?}, Websocket: {:?}",
508            self.http, self.ws
509        )
510    }
511}
512
513/// ServerInfo contains all the useful information regarding the API version, the available RPC calls, and subscriptions.
514struct ServerInfo {
515    rpc_methods: Vec<String>,
516    subscriptions: Vec<String>,
517    version: String,
518}
519
520impl SuiClient {
521    /// Returns a list of RPC methods supported by the node the client is connected to.
522    pub fn available_rpc_methods(&self) -> &Vec<String> {
523        &self.api.info.rpc_methods
524    }
525
526    /// Returns a list of streaming/subscription APIs supported by the node the client is connected to.
527    pub fn available_subscriptions(&self) -> &Vec<String> {
528        &self.api.info.subscriptions
529    }
530
531    /// Returns the API version information as a string.
532    ///
533    /// The format of this string is `<major>.<minor>.<patch>`, e.g., `1.6.0`,
534    /// and it is retrieved from the OpenRPC specification via the discover service method.
535    pub fn api_version(&self) -> &str {
536        &self.api.info.version
537    }
538
539    /// Verifies if the API version matches the server version and returns an error if they do not match.
540    pub fn check_api_version(&self) -> SuiRpcResult<()> {
541        let server_version = self.api_version();
542        let client_version = env!("CARGO_PKG_VERSION");
543        if server_version != client_version {
544            return Err(Error::ServerVersionMismatch {
545                client_version: client_version.to_string(),
546                server_version: server_version.to_string(),
547            });
548        };
549        Ok(())
550    }
551
552    /// Returns a reference to the coin read API.
553    pub fn coin_read_api(&self) -> &CoinReadApi {
554        &self.coin_read_api
555    }
556
557    /// Returns a reference to the event API.
558    pub fn event_api(&self) -> &EventApi {
559        &self.event_api
560    }
561
562    /// Returns a reference to the governance API.
563    pub fn governance_api(&self) -> &GovernanceApi {
564        &self.governance_api
565    }
566
567    /// Returns a reference to the quorum driver API.
568    pub fn quorum_driver_api(&self) -> &QuorumDriverApi {
569        &self.quorum_driver_api
570    }
571
572    /// Returns a reference to the read API.
573    pub fn read_api(&self) -> &ReadApi {
574        &self.read_api
575    }
576
577    /// Returns a reference to the transaction builder API.
578    pub fn transaction_builder(&self) -> &TransactionBuilder {
579        &self.transaction_builder
580    }
581
582    /// Returns a reference to the underlying http client.
583    pub fn http(&self) -> &HttpClient {
584        &self.api.http
585    }
586
587    /// Returns a reference to the underlying WebSocket client, if any.
588    pub fn ws(&self) -> Option<&WsClient> {
589        self.api.ws.as_ref()
590    }
591}
592
593#[async_trait]
594impl DataReader for ReadApi {
595    async fn get_owned_objects(
596        &self,
597        address: SuiAddress,
598        object_type: StructTag,
599    ) -> Result<Vec<ObjectInfo>, anyhow::Error> {
600        let mut result = vec![];
601        let query = Some(SuiObjectResponseQuery {
602            filter: Some(SuiObjectDataFilter::StructType(object_type)),
603            options: Some(
604                SuiObjectDataOptions::new()
605                    .with_previous_transaction()
606                    .with_type()
607                    .with_owner(),
608            ),
609        });
610
611        let mut has_next = true;
612        let mut cursor = None;
613
614        while has_next {
615            let ObjectsPage {
616                data,
617                next_cursor,
618                has_next_page,
619            } = self
620                .get_owned_objects(address, query.clone(), cursor, None)
621                .await?;
622            result.extend(
623                data.iter()
624                    .map(|r| r.clone().try_into())
625                    .collect::<Result<Vec<_>, _>>()?,
626            );
627            cursor = next_cursor;
628            has_next = has_next_page;
629        }
630        Ok(result)
631    }
632
633    async fn get_object_with_options(
634        &self,
635        object_id: ObjectID,
636        options: SuiObjectDataOptions,
637    ) -> Result<SuiObjectResponse, anyhow::Error> {
638        Ok(self.get_object_with_options(object_id, options).await?)
639    }
640
641    /// Returns the reference gas price as a u64 or an error otherwise
642    async fn get_reference_gas_price(&self) -> Result<u64, anyhow::Error> {
643        Ok(self.get_reference_gas_price().await?)
644    }
645}