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}