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