Skip to main content

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