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}