1use 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
49pub const MAX_GAS_BUDGET: u64 = 50000000000;
51pub 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>;
62type 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
93pub 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 pub const fn request_timeout(mut self, request_timeout: Duration) -> Self {
137 self.request_timeout = request_timeout;
138 self
139 }
140
141 #[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 #[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 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 pub async fn build_localnet(self) -> SuiClientResult<SuiClient> {
171 self.build(SUI_LOCAL_NETWORK_URL).await
172 }
173
174 pub async fn build_devnet(self) -> SuiClientResult<SuiClient> {
176 self.build(SUI_DEVNET_URL).await
177 }
178
179 pub async fn build_testnet(self) -> SuiClientResult<SuiClient> {
181 self.build(SUI_TESTNET_URL).await
182 }
183
184 #[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 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 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#[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
310struct 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 #[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 #[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 #[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 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 #[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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[derive(Clone, Debug)]
844#[non_exhaustive]
845pub struct GasBudgetOptions {
846 pub price: u64,
848
849 pub dry_run_budget: u64,
851
852 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
931const GAS_SAFE_OVERHEAD_MULTIPLIER: u64 = 1000;
935
936fn 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}