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}