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