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::{
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
48pub const MAX_GAS_BUDGET: u64 = 50000000000;
50pub 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>;
61type 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
92pub 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 pub const fn request_timeout(mut self, request_timeout: Duration) -> Self {
136 self.request_timeout = request_timeout;
137 self
138 }
139
140 #[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 #[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 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 pub async fn build_localnet(self) -> SuiClientResult<SuiClient> {
170 self.build(SUI_LOCAL_NETWORK_URL).await
171 }
172
173 pub async fn build_devnet(self) -> SuiClientResult<SuiClient> {
175 self.build(SUI_DEVNET_URL).await
176 }
177
178 pub async fn build_testnet(self) -> SuiClientResult<SuiClient> {
180 self.build(SUI_TESTNET_URL).await
181 }
182
183 #[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 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 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#[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
309struct 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 #[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 #[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 #[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 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 #[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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[derive(Clone, Debug)]
842#[non_exhaustive]
843pub struct GasBudgetOptions {
844 pub price: u64,
846
847 pub dry_run_budget: u64,
849
850 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
929const GAS_SAFE_OVERHEAD_MULTIPLIER: u64 = 1000;
933
934fn 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}