sui_jsonrpc/
client.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::fmt::{Debug, Formatter};
5use std::sync::Arc;
6use std::time::Duration;
7
8use futures_core::Stream;
9use jsonrpsee::core::client::ClientT;
10use jsonrpsee::rpc_params;
11use jsonrpsee::ws_client::{PingConfig, WsClient, WsClientBuilder};
12use jsonrpsee_http_client::{HeaderMap, HeaderValue, HttpClient, HttpClientBuilder};
13use serde_json::Value;
14use sui_sdk_types::{
15    Address,
16    Digest,
17    GasCostSummary,
18    GasPayment,
19    Input,
20    Object,
21    ObjectReference,
22    Transaction,
23    TransactionExpiration,
24    TransactionKind,
25    UserSignature,
26    Version,
27};
28
29use super::{CLIENT_SDK_TYPE_HEADER, CLIENT_SDK_VERSION_HEADER, CLIENT_TARGET_API_VERSION_HEADER};
30use crate::api::{CoinReadApiClient, ReadApiClient as _, WriteApiClient as _};
31use crate::error::JsonRpcClientError;
32use crate::msgs::{
33    Coin,
34    DryRunTransactionBlockResponse,
35    SuiExecutionStatus,
36    SuiObjectData,
37    SuiObjectDataError,
38    SuiObjectDataOptions,
39    SuiObjectResponse,
40    SuiObjectResponseError,
41    SuiObjectResponseQuery,
42    SuiTransactionBlockEffectsAPI as _,
43    SuiTransactionBlockResponse,
44    SuiTransactionBlockResponseOptions,
45};
46use crate::serde::encode_base64_default;
47
48/// Maximum possible budget.
49pub const MAX_GAS_BUDGET: u64 = 50000000000;
50/// Maximum number of objects that can be fetched via
51/// [multi_get_objects][crate::api::ReadApiClient::multi_get_objects].
52pub const MULTI_GET_OBJECT_MAX_SIZE: usize = 50;
53pub const SUI_COIN_TYPE: &str = "0x2::sui::SUI";
54pub const SUI_LOCAL_NETWORK_URL: &str = "http://127.0.0.1:9000";
55pub const SUI_LOCAL_NETWORK_WS: &str = "ws://127.0.0.1:9000";
56pub const SUI_LOCAL_NETWORK_GAS_URL: &str = "http://127.0.0.1:5003/gas";
57pub const SUI_DEVNET_URL: &str = "https://fullnode.devnet.sui.io:443";
58pub const SUI_TESTNET_URL: &str = "https://fullnode.testnet.sui.io:443";
59
60pub type SuiClientResult<T = ()> = Result<T, SuiClientError>;
61// Placeholder for errors that can't be added to `SuiClientError` yet since that would be a
62// breaking change.
63type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
64
65#[derive(thiserror::Error, Debug)]
66pub enum SuiClientError {
67    #[error("jsonrpsee client error: {0}")]
68    JsonRpcClient(#[from] JsonRpcClientError),
69    #[error("Data error: {0}")]
70    DataError(String),
71    #[error(
72        "Client/Server api version mismatch, client api version : {client_version}, server api version : {server_version}"
73    )]
74    ServerVersionMismatch {
75        client_version: String,
76        server_version: String,
77    },
78    #[error(
79        "Insufficient funds for address [{address}]; found balance {found}, requested: {requested}"
80    )]
81    InsufficientFunds {
82        address: Address,
83        found: u64,
84        requested: u64,
85    },
86    #[error("In object response: {0}")]
87    SuiObjectResponse(#[from] SuiObjectResponseError),
88    #[error("In object data: {0}")]
89    SuiObjectData(#[from] SuiObjectDataError),
90}
91
92/// A Sui client builder for connecting to the Sui network
93///
94/// By default the `maximum concurrent requests` is set to 256 and
95/// the `request timeout` is set to 60 seconds. These can be adjusted using the
96/// `max_concurrent_requests` function, and the `request_timeout` function.
97/// If you use the WebSocket, consider setting the `ws_ping_interval` field to a
98/// value of your choice to prevent the inactive WS subscription being
99/// disconnected due to proxy timeout.
100///
101/// # Examples
102///
103/// ```rust,no_run
104/// use sui_jsonrpc::client::SuiClientBuilder;
105/// #[tokio::main]
106/// async fn main() -> Result<(), color_eyre::eyre::Error> {
107///     let sui = SuiClientBuilder::default()
108///         .build("http://127.0.0.1:9000")
109///         .await?;
110///
111///     println!("Sui local network version: {:?}", sui.api_version());
112///     Ok(())
113/// }
114/// ```
115pub struct SuiClientBuilder {
116    request_timeout: Duration,
117    ws_url: Option<String>,
118    ws_ping_interval: Option<Duration>,
119    basic_auth: Option<(String, String)>,
120}
121
122impl Default for SuiClientBuilder {
123    fn default() -> Self {
124        Self {
125            request_timeout: Duration::from_secs(60),
126            ws_url: None,
127            ws_ping_interval: None,
128            basic_auth: None,
129        }
130    }
131}
132
133impl SuiClientBuilder {
134    /// Set the request timeout to the specified duration
135    pub const fn request_timeout(mut self, request_timeout: Duration) -> Self {
136        self.request_timeout = request_timeout;
137        self
138    }
139
140    /// Set the WebSocket URL for the Sui network
141    #[deprecated = "\
142        JSON-RPC subscriptions have been deprecated since at least mainnet-v1.28.3. \
143        See <https://github.com/MystenLabs/sui/releases/tag/mainnet-v1.28.3>\
144    "]
145    pub fn ws_url(mut self, url: impl AsRef<str>) -> Self {
146        self.ws_url = Some(url.as_ref().to_string());
147        self
148    }
149
150    /// Set the WebSocket ping interval
151    #[deprecated = "\
152        JSON-RPC subscriptions have been deprecated since at least mainnet-v1.28.3. \
153        See <https://github.com/MystenLabs/sui/releases/tag/mainnet-v1.28.3>\
154    "]
155    pub const fn ws_ping_interval(mut self, duration: Duration) -> Self {
156        self.ws_ping_interval = Some(duration);
157        self
158    }
159
160    /// Set the basic auth credentials for the HTTP client
161    pub fn basic_auth(mut self, username: impl AsRef<str>, password: impl AsRef<str>) -> Self {
162        self.basic_auth = Some((username.as_ref().to_string(), password.as_ref().to_string()));
163        self
164    }
165
166    /// Returns a [SuiClient] object that is ready to interact with the local
167    /// development network (by default it expects the Sui network to be
168    /// up and running at `127.0.0.1:9000`).
169    pub async fn build_localnet(self) -> SuiClientResult<SuiClient> {
170        self.build(SUI_LOCAL_NETWORK_URL).await
171    }
172
173    /// Returns a [SuiClient] object that is ready to interact with the Sui devnet.
174    pub async fn build_devnet(self) -> SuiClientResult<SuiClient> {
175        self.build(SUI_DEVNET_URL).await
176    }
177
178    /// Returns a [SuiClient] object that is ready to interact with the Sui testnet.
179    pub async fn build_testnet(self) -> SuiClientResult<SuiClient> {
180        self.build(SUI_TESTNET_URL).await
181    }
182
183    /// Returns a [SuiClient] object connected to the Sui network running at the URI provided.
184    #[allow(clippy::future_not_send)]
185    pub async fn build(self, http: impl AsRef<str>) -> SuiClientResult<SuiClient> {
186        let client_version = env!("CARGO_PKG_VERSION");
187        let mut headers = HeaderMap::new();
188        headers.insert(
189            CLIENT_TARGET_API_VERSION_HEADER,
190            // in rust, the client version is the same as the target api version
191            HeaderValue::from_static(client_version),
192        );
193        headers.insert(
194            CLIENT_SDK_VERSION_HEADER,
195            HeaderValue::from_static(client_version),
196        );
197        headers.insert(CLIENT_SDK_TYPE_HEADER, HeaderValue::from_static("rust"));
198
199        if let Some((username, password)) = self.basic_auth {
200            let auth = encode_base64_default(format!("{}:{}", username, password));
201            headers.insert(
202                http::header::AUTHORIZATION,
203                HeaderValue::from_str(&format!("Basic {}", auth))
204                    .expect("Failed creating HeaderValue for basic auth"),
205            );
206        }
207
208        let ws = if let Some(url) = self.ws_url {
209            let mut builder = WsClientBuilder::default()
210                .max_request_size(2 << 30)
211                .set_headers(headers.clone())
212                .request_timeout(self.request_timeout);
213
214            if let Some(duration) = self.ws_ping_interval {
215                builder = builder.enable_ws_ping(PingConfig::default().ping_interval(duration))
216            }
217
218            Some(builder.build(url).await?)
219        } else {
220            None
221        };
222
223        let http = HttpClientBuilder::default()
224            .max_request_size(2 << 30)
225            .set_headers(headers.clone())
226            .request_timeout(self.request_timeout)
227            .build(http)?;
228
229        let info = Self::get_server_info(&http, &ws).await?;
230
231        Ok(SuiClient {
232            http: Arc::new(http),
233            ws: Arc::new(ws),
234            info: Arc::new(info),
235        })
236    }
237
238    /// Return the server information as a `ServerInfo` structure.
239    ///
240    /// Fails with an error if it cannot call the RPC discover.
241    async fn get_server_info(
242        http: &HttpClient,
243        ws: &Option<WsClient>,
244    ) -> Result<ServerInfo, SuiClientError> {
245        let rpc_spec: Value = http.request("rpc.discover", rpc_params![]).await?;
246        let version = rpc_spec
247            .pointer("/info/version")
248            .and_then(|v| v.as_str())
249            .ok_or_else(|| {
250                SuiClientError::DataError(
251                    "Fail parsing server version from rpc.discover endpoint.".into(),
252                )
253            })?;
254        let rpc_methods = Self::parse_methods(&rpc_spec)?;
255
256        let subscriptions = if let Some(ws) = ws {
257            let rpc_spec: Value = ws.request("rpc.discover", rpc_params![]).await?;
258            Self::parse_methods(&rpc_spec)?
259        } else {
260            Vec::new()
261        };
262        Ok(ServerInfo {
263            rpc_methods,
264            subscriptions,
265            version: version.to_string(),
266        })
267    }
268
269    fn parse_methods(server_spec: &Value) -> Result<Vec<String>, SuiClientError> {
270        let methods = server_spec
271            .pointer("/methods")
272            .and_then(|methods| methods.as_array())
273            .ok_or_else(|| {
274                SuiClientError::DataError(
275                    "Fail parsing server information from rpc.discover endpoint.".into(),
276                )
277            })?;
278
279        Ok(methods
280            .iter()
281            .flat_map(|method| method["name"].as_str())
282            .map(|s| s.into())
283            .collect())
284    }
285}
286
287/// SuiClient is the basic type that provides all the necessary abstractions for interacting with the Sui network.
288///
289/// # Usage
290///
291/// Use [SuiClientBuilder] to build a [SuiClient].
292#[derive(Clone)]
293pub struct SuiClient {
294    http: Arc<HttpClient>,
295    ws: Arc<Option<WsClient>>,
296    info: Arc<ServerInfo>,
297}
298
299impl Debug for SuiClient {
300    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
301        write!(
302            f,
303            "RPC client. Http: {:?}, Websocket: {:?}",
304            self.http, self.ws
305        )
306    }
307}
308
309/// ServerInfo contains all the useful information regarding the API version, the available RPC calls, and subscriptions.
310struct ServerInfo {
311    rpc_methods: Vec<String>,
312    subscriptions: Vec<String>,
313    version: String,
314}
315
316impl SuiClient {
317    pub fn builder() -> SuiClientBuilder {
318        Default::default()
319    }
320
321    /// Returns a list of RPC methods supported by the node the client is connected to.
322    #[rustversion::attr(
323        stable,
324        expect(
325            clippy::missing_const_for_fn,
326            reason = "Not changing the public API right now"
327        )
328    )]
329    pub fn available_rpc_methods(&self) -> &Vec<String> {
330        &self.info.rpc_methods
331    }
332
333    /// Returns a list of streaming/subscription APIs supported by the node the client is connected to.
334    #[rustversion::attr(
335        stable,
336        expect(
337            clippy::missing_const_for_fn,
338            reason = "Not changing the public API right now"
339        )
340    )]
341    pub fn available_subscriptions(&self) -> &Vec<String> {
342        &self.info.subscriptions
343    }
344
345    /// Returns the API version information as a string.
346    ///
347    /// The format of this string is `<major>.<minor>.<patch>`, e.g., `1.6.0`,
348    /// and it is retrieved from the OpenRPC specification via the discover service method.
349    #[rustversion::attr(
350        stable,
351        expect(
352            clippy::missing_const_for_fn,
353            reason = "Not changing the public API right now"
354        )
355    )]
356    pub fn api_version(&self) -> &str {
357        &self.info.version
358    }
359
360    /// Verifies if the API version matches the server version and returns an error if they do not match.
361    pub fn check_api_version(&self) -> SuiClientResult<()> {
362        let server_version = self.api_version();
363        let client_version = env!("CARGO_PKG_VERSION");
364        if server_version != client_version {
365            return Err(SuiClientError::ServerVersionMismatch {
366                client_version: client_version.to_string(),
367                server_version: server_version.to_string(),
368            });
369        };
370        Ok(())
371    }
372
373    /// Returns a reference to the underlying http client.
374    #[rustversion::attr(
375        stable,
376        expect(
377            clippy::missing_const_for_fn,
378            reason = "Not changing the public API right now"
379        )
380    )]
381    pub fn http(&self) -> &HttpClient {
382        &self.http
383    }
384
385    /// Returns a reference to the underlying WebSocket client, if any.
386    #[rustversion::attr(
387        stable,
388        expect(
389            clippy::missing_const_for_fn,
390            reason = "Not changing the public API right now"
391        )
392    )]
393    pub fn ws(&self) -> Option<&WsClient> {
394        (*self.ws).as_ref()
395    }
396
397    pub async fn get_shared_oarg(&self, id: Address, mutable: bool) -> SuiClientResult<Input> {
398        let data = self
399            .http()
400            .get_object(id, Some(SuiObjectDataOptions::new().with_owner()))
401            .await?
402            .into_object()?;
403        Ok(data.shared_object_arg(mutable)?)
404    }
405
406    pub async fn get_imm_or_owned_oarg(&self, id: Address) -> SuiClientResult<Input> {
407        let data = self
408            .http()
409            .get_object(id, Some(SuiObjectDataOptions::new().with_owner()))
410            .await?
411            .into_object()?;
412        Ok(data.imm_or_owned_object_arg()?)
413    }
414
415    /// Query the PTB args for several objects.
416    ///
417    /// Mutability (for shared objects) defaults to `false`.
418    ///
419    /// This has **no** consistency guarantees, i.e., object versions may come from different
420    /// points in the chain's history (i.e., from different checkpoints).
421    pub async fn object_args<Iter>(
422        &self,
423        ids: Iter,
424    ) -> Result<impl Iterator<Item = Result<Input, BoxError>> + use<Iter>, BoxError>
425    where
426        Iter: IntoIterator<Item = Address> + Send,
427        Iter::IntoIter: Send,
428    {
429        let options = SuiObjectDataOptions::new().with_owner();
430        Ok(self
431            .multi_get_objects(ids, options)
432            .await?
433            .into_iter()
434            .map(|r| Ok(r.into_object()?.object_arg(false)?)))
435    }
436
437    /// Query the full object contents as a standard Sui type.
438    pub async fn full_object(&self, id: Address) -> Result<Object, BoxError> {
439        let options = SuiObjectDataOptions {
440            show_bcs: true,
441            show_owner: true,
442            show_storage_rebate: true,
443            show_previous_transaction: true,
444            ..Default::default()
445        };
446        Ok(self
447            .http()
448            .get_object(id, Some(options))
449            .await?
450            .into_object()?
451            .into_full_object()?)
452    }
453
454    /// Query the full contents for several objects.
455    ///
456    /// This has **no** consistency guarantees, i.e., object versions may come from different
457    /// points in the chain's history (i.e., from different checkpoints).
458    pub async fn full_objects<Iter>(
459        &self,
460        ids: Iter,
461    ) -> Result<impl Iterator<Item = Result<Object, BoxError>>, BoxError>
462    where
463        Iter: IntoIterator<Item = Address> + Send,
464        Iter::IntoIter: Send,
465    {
466        let options = SuiObjectDataOptions {
467            show_bcs: true,
468            show_owner: true,
469            show_storage_rebate: true,
470            show_previous_transaction: true,
471            ..Default::default()
472        };
473        Ok(self
474            .multi_get_objects(ids, options)
475            .await?
476            .into_iter()
477            .map(|r| Ok(r.into_object()?.into_full_object()?)))
478    }
479
480    /// Return the object data for a list of objects.
481    ///
482    /// This method works for any number of object ids.
483    pub async fn multi_get_objects<I>(
484        &self,
485        object_ids: I,
486        options: SuiObjectDataOptions,
487    ) -> SuiClientResult<Vec<SuiObjectResponse>>
488    where
489        I: IntoIterator<Item = Address> + Send,
490        I::IntoIter: Send,
491    {
492        let mut result = Vec::new();
493        for chunk in iter_chunks(object_ids, MULTI_GET_OBJECT_MAX_SIZE) {
494            if chunk.len() == 1 {
495                let elem = self
496                    .http()
497                    .get_object(chunk[0], Some(options.clone()))
498                    .await?;
499                result.push(elem);
500            } else {
501                let it = self
502                    .http()
503                    .multi_get_objects(chunk, Some(options.clone()))
504                    .await?;
505                result.extend(it);
506            }
507        }
508        Ok(result)
509    }
510
511    /// Helper to execute a transaction using standard Sui types.
512    ///
513    /// See [`JsonRpcClientErrorExt::as_error_object`] and [`ErrorObjectExt`] for how to inspect
514    /// the error in case a transaction failed before being included in a checkpoint.
515    ///
516    /// [`JsonRpcClientErrorExt::as_error_object`]: crate::error::JsonRpcClientErrorExt::as_error_object
517    /// [`ErrorObjectExt`]: crate::error::ErrorObjectExt
518    pub async fn submit_transaction(
519        &self,
520        tx_data: &Transaction,
521        signatures: &[UserSignature],
522        options: Option<SuiTransactionBlockResponseOptions>,
523    ) -> Result<SuiTransactionBlockResponse, JsonRpcClientError> {
524        let tx_bytes =
525            encode_base64_default(bcs::to_bytes(tx_data).expect("Transaction is BCS-compatible"));
526        self.http()
527            .execute_transaction_block(
528                tx_bytes,
529                signatures.iter().map(UserSignature::to_base64).collect(),
530                options,
531                None,
532            )
533            .await
534    }
535
536    /// Helper to dry run a transaction kind for its effects.
537    ///
538    /// This sets the gas budget to the maximum possible. If you want to test different values,
539    /// manually perform the dry-run using the inner [`Self::http`] client.
540    pub async fn dry_run_transaction(
541        &self,
542        tx_kind: &TransactionKind,
543        sender: Address,
544        gas_price: u64,
545    ) -> Result<DryRunTransactionBlockResponse, JsonRpcClientError> {
546        let tx_data = Transaction {
547            kind: tx_kind.clone(),
548            sender,
549            gas_payment: GasPayment {
550                objects: vec![],
551                owner: sender,
552                price: gas_price,
553                budget: MAX_GAS_BUDGET,
554            },
555            expiration: TransactionExpiration::None,
556        };
557        let tx_bytes = encode_base64_default(
558            bcs::to_bytes(&tx_data).expect("Transaction serialization shouldn't fail"),
559        );
560        self.http().dry_run_transaction_block(tx_bytes).await
561    }
562
563    /// Estimate a budget for the transaction by dry-running it.
564    ///
565    /// Uses default [`GasBudgetOptions`] to compute the cost estimate.
566    pub async fn gas_budget(
567        &self,
568        tx_kind: &TransactionKind,
569        sender: Address,
570        price: u64,
571    ) -> Result<u64, DryRunError> {
572        let options = GasBudgetOptions::new(price);
573        self.gas_budget_with_options(tx_kind, sender, options).await
574    }
575
576    /// Estimate a budget for the transaction by dry-running it.
577    pub async fn gas_budget_with_options(
578        &self,
579        tx_kind: &TransactionKind,
580        sender: Address,
581        options: GasBudgetOptions,
582    ) -> Result<u64, DryRunError> {
583        let tx_data = Transaction {
584            kind: tx_kind.clone(),
585            sender,
586            gas_payment: GasPayment {
587                objects: vec![],
588                owner: sender,
589                price: options.price,
590                budget: options.dry_run_budget,
591            },
592            expiration: TransactionExpiration::None,
593        };
594        let tx_bytes = encode_base64_default(
595            bcs::to_bytes(&tx_data).expect("Transaction serialization shouldn't fail"),
596        );
597        let response = self.http().dry_run_transaction_block(tx_bytes).await?;
598        if let SuiExecutionStatus::Failure { error } = response.effects.status() {
599            return Err(DryRunError::Execution(error.clone(), response));
600        }
601
602        let budget = {
603            let safe_overhead = options.safe_overhead_multiplier * options.price;
604            estimate_gas_budget_from_gas_cost(response.effects.gas_cost_summary(), safe_overhead)
605        };
606        Ok(budget)
607    }
608
609    /// Build the gas data for a transaction by querying the node for gas objects.
610    pub async fn get_gas_data(
611        &self,
612        tx_kind: &TransactionKind,
613        sponsor: Address,
614        budget: u64,
615        price: u64,
616    ) -> Result<GasPayment, GetGasDataError> {
617        let exclude = if let TransactionKind::ProgrammableTransaction(ptb) = tx_kind {
618            use sui_sdk_types::Input::*;
619
620            ptb.inputs
621                .iter()
622                .filter_map(|i| match i {
623                    Pure { .. } => None,
624                    Shared { object_id, .. } => Some(*object_id),
625                    ImmutableOrOwned(oref) | Receiving(oref) => Some(*oref.object_id()),
626                    _ => panic!("unknown Input type"),
627                })
628                .collect()
629        } else {
630            vec![]
631        };
632
633        if budget < price {
634            return Err(GetGasDataError::BudgetTooSmall { budget, price });
635        }
636
637        let objects = self
638            .get_gas_payment(sponsor, budget, &exclude)
639            .await
640            .map_err(GetGasDataError::from_not_enough_gas)?;
641
642        Ok(GasPayment {
643            objects: objects
644                .into_iter()
645                .map(|(object_id, version, digest)| {
646                    ObjectReference::new(object_id, version, digest)
647                })
648                .collect(),
649            owner: sponsor,
650            price,
651            budget,
652        })
653    }
654
655    /// Query the node for gas objects to fulfill a certain budget.
656    ///
657    /// `exclude`s certain object ids from being part of the returned objects.
658    pub async fn get_gas_payment(
659        &self,
660        sponsor: Address,
661        budget: u64,
662        exclude: &[Address],
663    ) -> Result<Vec<(Address, Version, Digest)>, NotEnoughGasError> {
664        Ok(self
665            .coins_for_amount(sponsor, Some("0x2::sui::SUI".to_owned()), budget, exclude)
666            .await
667            .map_err(|inner| NotEnoughGasError {
668                sponsor,
669                budget,
670                inner,
671            })?
672            .into_iter()
673            .map(|c| c.object_ref())
674            .collect())
675    }
676
677    #[deprecated(since = "0.14.5", note = "use SuiClient::coins_for_amount")]
678    pub async fn select_coins(
679        &self,
680        address: Address,
681        coin_type: Option<String>,
682        amount: u64,
683        exclude: Vec<Address>,
684    ) -> SuiClientResult<Vec<Coin>> {
685        self.coins_for_amount(address, coin_type, amount, &exclude)
686            .await
687    }
688
689    /// Return a list of coins for the given address, or an error upon failure.
690    ///
691    /// Note that the function selects coins to meet or exceed the requested `amount`.
692    /// If that it is not possible, it will fail with an insufficient fund error.
693    ///
694    /// The coins can be filtered by `coin_type` (e.g., 0x168da5bf1f48dafc111b0a488fa454aca95e0b5e::usdc::USDC)
695    /// or use `None` to use the default `Coin<SUI>`.
696    ///
697    /// # Examples
698    ///
699    /// ```rust,no_run
700    /// use sui_jsonrpc::client::SuiClientBuilder;
701    /// use sui_sdk_types::Address;
702    ///
703    /// #[tokio::main]
704    /// async fn main() -> color_eyre::Result<()> {
705    ///     let sui = SuiClientBuilder::default().build_localnet().await?;
706    ///     let address = "0x0000....0000".parse()?;
707    ///     let coins = sui
708    ///         .select_coins(address, None, 5, vec![])
709    ///         .await?;
710    ///     Ok(())
711    /// }
712    /// ```
713    pub async fn coins_for_amount(
714        &self,
715        address: Address,
716        coin_type: Option<String>,
717        amount: u64,
718        exclude: &[Address],
719    ) -> SuiClientResult<Vec<Coin>> {
720        use futures_util::{TryStreamExt as _, future};
721        let mut coins = vec![];
722        let mut total = 0;
723        let mut stream = std::pin::pin!(
724            self.coins_for_address(address, coin_type, None)
725                .try_filter(|c| future::ready(!exclude.contains(&c.coin_object_id)))
726        );
727
728        while let Some(coin) = stream.try_next().await? {
729            total += coin.balance;
730            coins.push(coin);
731            if total >= amount {
732                return Ok(coins);
733            }
734        }
735
736        Err(SuiClientError::InsufficientFunds {
737            address,
738            found: total,
739            requested: amount,
740        })
741    }
742
743    /// Return a stream of coins for the given address, or an error upon failure.
744    ///
745    /// This simply wraps a paginated query. Use `page_size` to control the inner query's page
746    /// size.
747    ///
748    /// The coins can be filtered by `coin_type` (e.g., 0x168da5bf1f48dafc111b0a488fa454aca95e0b5e::usdc::USDC)
749    /// or use `None` to use the default `Coin<SUI>`.
750    ///
751    /// # Examples
752    ///
753    /// ```rust,no_run
754    /// use sui_jsonrpc::client::SuiClientBuilder;
755    /// use sui_sdk_types::Address;
756    /// use futures::TryStreamExt as _;
757    ///
758    /// #[tokio::main]
759    /// async fn main() -> color_eyre::Result<()> {
760    ///     let sui = SuiClientBuilder::default().build_localnet().await?;
761    ///     let address = "0x0000....0000".parse()?;
762    ///     let mut coins = std::pin::pin!(sui.coins_for_address(address, None, Some(5)));
763    ///
764    ///     while let Some(coin) = coins.try_next().await? {
765    ///         println!("{coin:?}");
766    ///     }
767    ///     Ok(())
768    /// }
769    /// ```
770    pub fn coins_for_address(
771        &self,
772        address: Address,
773        coin_type: Option<String>,
774        page_size: Option<u32>,
775    ) -> impl Stream<Item = SuiClientResult<Coin>> + Send + '_ {
776        async_stream::try_stream! {
777            let mut has_next_page = true;
778            let mut cursor = None;
779
780            while has_next_page {
781                let page = self
782                    .http()
783                    .get_coins(address, coin_type.clone(), cursor, page_size.map(|u| u as usize))
784                    .await?;
785
786                for coin in page.data
787                {
788                    yield coin;
789                }
790
791                has_next_page = page.has_next_page;
792                cursor = page.next_cursor;
793            }
794        }
795    }
796
797    /// Return a stream of objects owned by the given address.
798    ///
799    /// This simply wraps a paginated query. Use `page_size` to control the inner query's page
800    /// size.
801    pub fn owned_objects(
802        &self,
803        owner: Address,
804        query: Option<SuiObjectResponseQuery>,
805        page_size: Option<u32>,
806    ) -> impl Stream<Item = SuiClientResult<SuiObjectData>> + Send + '_ {
807        use crate::api::IndexerApiClient as _;
808        async_stream::try_stream! {
809            let mut has_next_page = true;
810            let mut cursor = None;
811
812            while has_next_page {
813                let page = self
814                    .http()
815                    .get_owned_objects(owner, query.clone(), cursor, page_size.map(|u| u as usize)).await?;
816
817                for data in page.data {
818                    yield data.into_object()?;
819                }
820                has_next_page = page.has_next_page;
821                cursor = page.next_cursor;
822            }
823        }
824    }
825
826    /// Get the latest object reference for an ID from the node.
827    pub async fn latest_object_ref(
828        &self,
829        object_id: Address,
830    ) -> SuiClientResult<(Address, Version, Digest)> {
831        Ok(self
832            .http()
833            .get_object(object_id, Some(SuiObjectDataOptions::default()))
834            .await?
835            .into_object()?
836            .object_ref())
837    }
838}
839
840/// Parameters for computing the gas budget for a transaction using a dry-run.
841#[derive(Clone, Debug)]
842#[non_exhaustive]
843pub struct GasBudgetOptions {
844    /// The gas price. Must be set via [`Self::new`].
845    pub price: u64,
846
847    /// The budget for the dry-run.
848    pub dry_run_budget: u64,
849
850    /// Multiplier on the gas price. The result is a balance to add to both the computation and net
851    /// gas costs to account for possible fluctuations when the transaction is actually submitted.
852    pub safe_overhead_multiplier: u64,
853}
854
855impl GasBudgetOptions {
856    #[expect(
857        clippy::missing_const_for_fn,
858        reason = "We might evolve the defaults to use non-const expressions"
859    )]
860    pub fn new(price: u64) -> Self {
861        Self {
862            price,
863            dry_run_budget: MAX_GAS_BUDGET,
864            safe_overhead_multiplier: GAS_SAFE_OVERHEAD_MULTIPLIER,
865        }
866    }
867}
868
869#[derive(thiserror::Error, Debug)]
870#[expect(
871    clippy::large_enum_variant,
872    reason = "Boxing now would break backwards compatibility"
873)]
874pub enum DryRunError {
875    #[error("Error in dry run: {0}")]
876    Execution(String, DryRunTransactionBlockResponse),
877    #[error("In JSON-RPC client: {0}")]
878    Client(#[from] JsonRpcClientError),
879}
880
881#[derive(thiserror::Error, Debug)]
882pub enum GetGasDataError {
883    #[error("In JSON-RPC client: {0}")]
884    Client(#[from] JsonRpcClientError),
885    #[error(
886        "Gas budget {budget} is less than the gas price {price}. \
887           The gas budget must be at least the gas price of {price}."
888    )]
889    BudgetTooSmall { budget: u64, price: u64 },
890    #[error(
891        "Cannot find gas coins for address [{sponsor}] \
892        with amount sufficient for the required gas amount [{budget}]. \
893        Caused by {inner}"
894    )]
895    NotEnoughGas {
896        sponsor: Address,
897        budget: u64,
898        inner: SuiClientError,
899    },
900}
901
902impl GetGasDataError {
903    fn from_not_enough_gas(e: NotEnoughGasError) -> Self {
904        let NotEnoughGasError {
905            sponsor,
906            budget,
907            inner,
908        } = e;
909        Self::NotEnoughGas {
910            sponsor,
911            budget,
912            inner,
913        }
914    }
915}
916
917#[derive(thiserror::Error, Debug)]
918#[error(
919    "Cannot find gas coins for address [{sponsor}] \
920        with amount sufficient for the required gas amount [{budget}]. \
921        Caused by {inner}"
922)]
923pub struct NotEnoughGasError {
924    sponsor: Address,
925    budget: u64,
926    inner: SuiClientError,
927}
928
929/// Multiplier on the gas price for computing gas budgets from dry-runs.
930///
931/// Same value as used in the Sui CLI.
932const GAS_SAFE_OVERHEAD_MULTIPLIER: u64 = 1000;
933
934/// Use primarily on the gas cost of dry-runs.
935///
936/// Same as used in the Sui CLI.
937///
938/// # Arguments
939/// - `gas_cost_summary`: gas cost breakdown
940/// - `safe_overhead`: balance to add to both the computation and net gas costs to account for
941///   possible fluctuations when the transaction is actually submitted.
942fn estimate_gas_budget_from_gas_cost(gas_cost_summary: &GasCostSummary, safe_overhead: u64) -> u64 {
943    let computation_cost_with_overhead = gas_cost_summary.computation_cost + safe_overhead;
944
945    let gas_usage_with_overhead = gas_cost_summary.net_gas_usage() + safe_overhead as i64;
946    computation_cost_with_overhead.max(gas_usage_with_overhead.max(0) as u64)
947}
948
949fn iter_chunks<I>(iter: I, chunk_size: usize) -> impl Iterator<Item = Vec<I::Item>> + Send
950where
951    I: IntoIterator,
952    I::IntoIter: Send,
953{
954    let mut iter = iter.into_iter();
955    std::iter::from_fn(move || {
956        let elem = iter.next()?;
957        let mut v = Vec::with_capacity(chunk_size);
958        v.push(elem);
959        v.extend(iter.by_ref().take(chunk_size - 1));
960        Some(v)
961    })
962}