1#[cfg(feature = "coin-cache")]
2use std::sync::Arc;
3use std::{collections::HashMap, fmt::Debug, net::SocketAddr};
4
5mod cache;
6mod retry_util;
7mod retryable_client;
8mod supported_fuel_core_version;
9mod supported_versions;
10
11use crate::provider::cache::CacheableRpcs;
12pub use cache::TtlConfig;
13use cache::{CachedClient, SystemClock};
14use chrono::{DateTime, Utc};
15use fuel_core_client::client::{
16 pagination::{PageDirection, PaginatedResult, PaginationRequest},
17 types::{
18 balance::Balance,
19 contract::ContractBalance,
20 gas_price::{EstimateGasPrice, LatestGasPrice},
21 },
22};
23use fuel_core_types::services::executor::TransactionExecutionResult;
24use fuel_tx::{
25 AssetId, ConsensusParameters, Receipt, Transaction as FuelTransaction, TxId, UtxoId,
26};
27use fuel_types::{Address, BlockHeight, Bytes32, Nonce};
28#[cfg(feature = "coin-cache")]
29use fuels_core::types::coin_type_id::CoinTypeId;
30use fuels_core::{
31 constants::{DEFAULT_GAS_ESTIMATION_BLOCK_HORIZON, DEFAULT_GAS_ESTIMATION_TOLERANCE},
32 types::{
33 bech32::{Bech32Address, Bech32ContractId},
34 block::{Block, Header},
35 chain_info::ChainInfo,
36 coin::Coin,
37 coin_type::CoinType,
38 errors::Result,
39 message::Message,
40 message_proof::MessageProof,
41 node_info::NodeInfo,
42 transaction::{Transaction, Transactions},
43 transaction_builders::{Blob, BlobId},
44 transaction_response::TransactionResponse,
45 tx_status::TxStatus,
46 DryRun, DryRunner,
47 },
48};
49pub use retry_util::{Backoff, RetryConfig};
50pub use supported_fuel_core_version::SUPPORTED_FUEL_CORE_VERSION;
51use tai64::Tai64;
52#[cfg(feature = "coin-cache")]
53use tokio::sync::Mutex;
54
55#[cfg(feature = "coin-cache")]
56use crate::coin_cache::CoinsCache;
57use crate::provider::retryable_client::RetryableClient;
58
59const NUM_RESULTS_PER_REQUEST: i32 = 100;
60
61#[derive(Debug, Clone, PartialEq)]
62pub struct TransactionCost {
64 pub gas_price: u64,
65 pub metered_bytes_size: u64,
66 pub total_fee: u64,
67 pub script_gas: u64,
68 pub total_gas: u64,
69}
70pub(crate) struct ResourceQueries {
73 utxos: Vec<UtxoId>,
74 messages: Vec<Nonce>,
75 asset_id: Option<AssetId>,
76 amount: u64,
77}
78
79impl ResourceQueries {
80 pub fn exclusion_query(&self) -> Option<(Vec<UtxoId>, Vec<Nonce>)> {
81 if self.utxos.is_empty() && self.messages.is_empty() {
82 return None;
83 }
84
85 Some((self.utxos.clone(), self.messages.clone()))
86 }
87
88 pub fn spend_query(&self, base_asset_id: AssetId) -> Vec<(AssetId, u64, Option<u32>)> {
89 vec![(self.asset_id.unwrap_or(base_asset_id), self.amount, None)]
90 }
91}
92
93#[derive(Default)]
94pub struct ResourceFilter {
96 pub from: Bech32Address,
97 pub asset_id: Option<AssetId>,
98 pub amount: u64,
99 pub excluded_utxos: Vec<UtxoId>,
100 pub excluded_message_nonces: Vec<Nonce>,
101}
102impl ResourceFilter {
105 pub fn owner(&self) -> Address {
106 (&self.from).into()
107 }
108
109 pub(crate) fn resource_queries(&self) -> ResourceQueries {
110 ResourceQueries {
111 utxos: self.excluded_utxos.clone(),
112 messages: self.excluded_message_nonces.clone(),
113 asset_id: self.asset_id,
114 amount: self.amount,
115 }
116 }
117}
118
119#[derive(Debug, Clone)]
123pub struct Provider {
124 cached_client: CachedClient<RetryableClient>,
125 #[cfg(feature = "coin-cache")]
126 coins_cache: Arc<Mutex<CoinsCache>>,
127}
128
129impl Provider {
130 pub async fn from(addr: impl Into<SocketAddr>) -> Result<Self> {
131 let addr = addr.into();
132 Self::connect(format!("http://{addr}")).await
133 }
134
135 pub fn set_cache_ttl(&mut self, ttl: TtlConfig) {
136 self.cached_client.set_ttl(ttl);
137 }
138
139 pub async fn clear_cache(&self) {
140 self.cached_client.clear().await;
141 }
142
143 pub async fn healthy(&self) -> Result<bool> {
144 Ok(self.uncached_client().health().await?)
145 }
146
147 pub async fn connect(url: impl AsRef<str>) -> Result<Provider> {
149 let client = CachedClient::new(
150 RetryableClient::connect(&url, Default::default()).await?,
151 TtlConfig::default(),
152 SystemClock,
153 );
154
155 Ok(Self {
156 cached_client: client,
157 #[cfg(feature = "coin-cache")]
158 coins_cache: Default::default(),
159 })
160 }
161
162 pub fn url(&self) -> &str {
163 self.uncached_client().url()
164 }
165
166 pub async fn blob(&self, blob_id: BlobId) -> Result<Option<Blob>> {
167 Ok(self
168 .uncached_client()
169 .blob(blob_id.into())
170 .await?
171 .map(|blob| Blob::new(blob.bytecode)))
172 }
173
174 pub async fn blob_exists(&self, blob_id: BlobId) -> Result<bool> {
175 Ok(self.uncached_client().blob_exists(blob_id.into()).await?)
176 }
177
178 pub async fn send_transaction_and_await_commit<T: Transaction>(
180 &self,
181 tx: T,
182 ) -> Result<TxStatus> {
183 #[cfg(feature = "coin-cache")]
184 let base_asset_id = *self.consensus_parameters().await?.base_asset_id();
185
186 #[cfg(feature = "coin-cache")]
187 self.check_inputs_already_in_cache(&tx.used_coins(&base_asset_id))
188 .await?;
189
190 let tx = self.prepare_transaction_for_sending(tx).await?;
191 let tx_status = self
192 .uncached_client()
193 .submit_and_await_commit(&tx.clone().into())
194 .await?
195 .into();
196
197 #[cfg(feature = "coin-cache")]
198 if matches!(
199 tx_status,
200 TxStatus::SqueezedOut { .. } | TxStatus::Revert { .. }
201 ) {
202 self.coins_cache
203 .lock()
204 .await
205 .remove_items(tx.used_coins(&base_asset_id))
206 }
207
208 Ok(tx_status)
209 }
210
211 async fn prepare_transaction_for_sending<T: Transaction>(&self, mut tx: T) -> Result<T> {
212 let consensus_parameters = self.consensus_parameters().await?;
213 tx.precompute(&consensus_parameters.chain_id())?;
214
215 let chain_info = self.chain_info().await?;
216 let Header {
217 height: latest_block_height,
218 state_transition_bytecode_version: latest_chain_executor_version,
219 ..
220 } = chain_info.latest_block.header;
221
222 if tx.is_using_predicates() {
223 tx.estimate_predicates(self, Some(latest_chain_executor_version))
224 .await?;
225 tx.clone()
226 .validate_predicates(&consensus_parameters, latest_block_height)?;
227 }
228
229 Ok(tx)
230 }
231
232 pub async fn send_transaction<T: Transaction>(&self, tx: T) -> Result<TxId> {
233 let tx = self.prepare_transaction_for_sending(tx).await?;
234 self.submit(tx).await
235 }
236
237 pub async fn await_transaction_commit<T: Transaction>(&self, id: TxId) -> Result<TxStatus> {
238 Ok(self
239 .uncached_client()
240 .await_transaction_commit(&id)
241 .await?
242 .into())
243 }
244
245 #[cfg(not(feature = "coin-cache"))]
246 async fn submit<T: Transaction>(&self, tx: T) -> Result<TxId> {
247 Ok(self.uncached_client().submit(&tx.into()).await?)
248 }
249
250 #[cfg(feature = "coin-cache")]
251 async fn find_in_cache<'a>(
252 &self,
253 coin_ids: impl IntoIterator<Item = (&'a (Bech32Address, AssetId), &'a Vec<CoinTypeId>)>,
254 ) -> Option<((Bech32Address, AssetId), CoinTypeId)> {
255 let mut locked_cache = self.coins_cache.lock().await;
256
257 for (key, ids) in coin_ids {
258 let items = locked_cache.get_active(key);
259
260 if items.is_empty() {
261 continue;
262 }
263
264 for id in ids {
265 if items.contains(id) {
266 return Some((key.clone(), id.clone()));
267 }
268 }
269 }
270
271 None
272 }
273
274 #[cfg(feature = "coin-cache")]
275 async fn check_inputs_already_in_cache<'a>(
276 &self,
277 coin_ids: impl IntoIterator<Item = (&'a (Bech32Address, AssetId), &'a Vec<CoinTypeId>)>,
278 ) -> Result<()> {
279 use fuels_core::types::errors::{transaction, Error};
280
281 if let Some(((addr, asset_id), coin_type_id)) = self.find_in_cache(coin_ids).await {
282 let msg = match coin_type_id {
283 CoinTypeId::UtxoId(utxo_id) => format!("coin with utxo_id: `{utxo_id:x}`"),
284 CoinTypeId::Nonce(nonce) => format!("message with nonce: `{nonce}`"),
285 };
286 Err(Error::Transaction(transaction::Reason::Validation(
287 format!("{msg} was submitted recently in a transaction - attempting to spend it again will result in an error. Wallet address: `{addr}`, asset id: `{asset_id}`"),
288 )))
289 } else {
290 Ok(())
291 }
292 }
293
294 #[cfg(feature = "coin-cache")]
295 async fn submit<T: Transaction>(&self, tx: T) -> Result<TxId> {
296 let consensus_parameters = self.consensus_parameters().await?;
297 let base_asset_id = consensus_parameters.base_asset_id();
298
299 let used_utxos = tx.used_coins(base_asset_id);
300 self.check_inputs_already_in_cache(&used_utxos).await?;
301
302 let tx_id = self.uncached_client().submit(&tx.into()).await?;
303 self.coins_cache.lock().await.insert_multiple(used_utxos);
304
305 Ok(tx_id)
306 }
307
308 pub async fn tx_status(&self, tx_id: &TxId) -> Result<TxStatus> {
309 Ok(self
310 .uncached_client()
311 .transaction_status(tx_id)
312 .await?
313 .into())
314 }
315
316 pub async fn chain_info(&self) -> Result<ChainInfo> {
317 Ok(self.uncached_client().chain_info().await?.into())
318 }
319
320 pub async fn consensus_parameters(&self) -> Result<ConsensusParameters> {
321 self.cached_client.consensus_parameters().await
322 }
323
324 pub async fn node_info(&self) -> Result<NodeInfo> {
325 Ok(self.cached_client.node_info().await?.into())
326 }
327
328 pub async fn latest_gas_price(&self) -> Result<LatestGasPrice> {
329 Ok(self.uncached_client().latest_gas_price().await?)
330 }
331
332 pub async fn estimate_gas_price(&self, block_horizon: u32) -> Result<EstimateGasPrice> {
333 Ok(self
334 .uncached_client()
335 .estimate_gas_price(block_horizon)
336 .await?)
337 }
338
339 pub async fn dry_run(&self, tx: impl Transaction) -> Result<TxStatus> {
340 let [tx_status] = self
341 .uncached_client()
342 .dry_run(Transactions::new().insert(tx).as_slice())
343 .await?
344 .into_iter()
345 .map(Into::into)
346 .collect::<Vec<_>>()
347 .try_into()
348 .expect("should have only one element");
349
350 Ok(tx_status)
351 }
352
353 pub async fn dry_run_multiple(
354 &self,
355 transactions: Transactions,
356 ) -> Result<Vec<(TxId, TxStatus)>> {
357 Ok(self
358 .uncached_client()
359 .dry_run(transactions.as_slice())
360 .await?
361 .into_iter()
362 .map(|execution_status| (execution_status.id, execution_status.into()))
363 .collect())
364 }
365
366 pub async fn dry_run_opt(
367 &self,
368 tx: impl Transaction,
369 utxo_validation: bool,
370 gas_price: Option<u64>,
371 ) -> Result<TxStatus> {
372 let [tx_status] = self
373 .uncached_client()
374 .dry_run_opt(
375 Transactions::new().insert(tx).as_slice(),
376 Some(utxo_validation),
377 gas_price,
378 )
379 .await?
380 .into_iter()
381 .map(Into::into)
382 .collect::<Vec<_>>()
383 .try_into()
384 .expect("should have only one element");
385
386 Ok(tx_status)
387 }
388
389 pub async fn dry_run_opt_multiple(
390 &self,
391 transactions: Transactions,
392 utxo_validation: bool,
393 gas_price: Option<u64>,
394 ) -> Result<Vec<(TxId, TxStatus)>> {
395 Ok(self
396 .uncached_client()
397 .dry_run_opt(transactions.as_slice(), Some(utxo_validation), gas_price)
398 .await?
399 .into_iter()
400 .map(|execution_status| (execution_status.id, execution_status.into()))
401 .collect())
402 }
403
404 pub async fn get_coins(&self, from: &Bech32Address, asset_id: AssetId) -> Result<Vec<Coin>> {
406 let mut coins: Vec<Coin> = vec![];
407 let mut cursor = None;
408
409 loop {
410 let response = self
411 .uncached_client()
412 .coins(
413 &from.into(),
414 Some(&asset_id),
415 PaginationRequest {
416 cursor: cursor.clone(),
417 results: NUM_RESULTS_PER_REQUEST,
418 direction: PageDirection::Forward,
419 },
420 )
421 .await?;
422
423 if response.results.is_empty() {
424 break;
425 }
426
427 coins.extend(response.results.into_iter().map(Into::into));
428 cursor = response.cursor;
429 }
430
431 Ok(coins)
432 }
433
434 async fn request_coins_to_spend(&self, filter: ResourceFilter) -> Result<Vec<CoinType>> {
435 let queries = filter.resource_queries();
436
437 let consensus_parameters = self.consensus_parameters().await?;
438 let base_asset_id = *consensus_parameters.base_asset_id();
439
440 let res = self
441 .uncached_client()
442 .coins_to_spend(
443 &filter.owner(),
444 queries.spend_query(base_asset_id),
445 queries.exclusion_query(),
446 )
447 .await?
448 .into_iter()
449 .flatten()
450 .map(CoinType::from)
451 .collect();
452
453 Ok(res)
454 }
455
456 #[cfg(not(feature = "coin-cache"))]
460 pub async fn get_spendable_resources(&self, filter: ResourceFilter) -> Result<Vec<CoinType>> {
461 self.request_coins_to_spend(filter).await
462 }
463
464 #[cfg(feature = "coin-cache")]
469 pub async fn get_spendable_resources(
470 &self,
471 mut filter: ResourceFilter,
472 ) -> Result<Vec<CoinType>> {
473 self.extend_filter_with_cached(&mut filter).await?;
474
475 self.request_coins_to_spend(filter).await
476 }
477
478 #[cfg(feature = "coin-cache")]
479 async fn extend_filter_with_cached(&self, filter: &mut ResourceFilter) -> Result<()> {
480 let consensus_parameters = self.consensus_parameters().await?;
481 let mut cache = self.coins_cache.lock().await;
482 let asset_id = filter
483 .asset_id
484 .unwrap_or(*consensus_parameters.base_asset_id());
485 let used_coins = cache.get_active(&(filter.from.clone(), asset_id));
486
487 let excluded_utxos = used_coins
488 .iter()
489 .filter_map(|coin_id| match coin_id {
490 CoinTypeId::UtxoId(utxo_id) => Some(utxo_id),
491 _ => None,
492 })
493 .cloned()
494 .collect::<Vec<_>>();
495
496 let excluded_message_nonces = used_coins
497 .iter()
498 .filter_map(|coin_id| match coin_id {
499 CoinTypeId::Nonce(nonce) => Some(nonce),
500 _ => None,
501 })
502 .cloned()
503 .collect::<Vec<_>>();
504
505 filter.excluded_utxos.extend(excluded_utxos);
506 filter
507 .excluded_message_nonces
508 .extend(excluded_message_nonces);
509
510 Ok(())
511 }
512
513 pub async fn get_asset_balance(
517 &self,
518 address: &Bech32Address,
519 asset_id: AssetId,
520 ) -> Result<u64> {
521 Ok(self
522 .uncached_client()
523 .balance(&address.into(), Some(&asset_id))
524 .await?)
525 }
526
527 pub async fn get_contract_asset_balance(
529 &self,
530 contract_id: &Bech32ContractId,
531 asset_id: AssetId,
532 ) -> Result<u64> {
533 Ok(self
534 .uncached_client()
535 .contract_balance(&contract_id.into(), Some(&asset_id))
536 .await?)
537 }
538
539 pub async fn get_balances(&self, address: &Bech32Address) -> Result<HashMap<String, u128>> {
543 let mut balances = HashMap::new();
544
545 let mut register_balances = |results: Vec<_>| {
546 let pairs = results.into_iter().map(
547 |Balance {
548 owner: _,
549 amount,
550 asset_id,
551 }| (asset_id.to_string(), amount),
552 );
553 balances.extend(pairs);
554 };
555
556 let indexation_flags = self.cached_client.node_info().await?.indexation;
557 if indexation_flags.balances {
558 let mut cursor = None;
559 loop {
560 let pagination = PaginationRequest {
561 cursor: cursor.clone(),
562 results: NUM_RESULTS_PER_REQUEST,
563 direction: PageDirection::Forward,
564 };
565 let response = self
566 .uncached_client()
567 .balances(&address.into(), pagination)
568 .await?;
569
570 if response.results.is_empty() {
571 break;
572 }
573
574 register_balances(response.results);
575 cursor = response.cursor;
576 }
577 } else {
578 let pagination = PaginationRequest {
579 cursor: None,
580 results: 9999,
581 direction: PageDirection::Forward,
582 };
583 let response = self
584 .uncached_client()
585 .balances(&address.into(), pagination)
586 .await?;
587
588 register_balances(response.results)
589 }
590
591 Ok(balances)
592 }
593
594 pub async fn get_contract_balances(
596 &self,
597 contract_id: &Bech32ContractId,
598 ) -> Result<HashMap<AssetId, u64>> {
599 let mut contract_balances = HashMap::new();
600 let mut cursor = None;
601
602 loop {
603 let response = self
604 .uncached_client()
605 .contract_balances(
606 &contract_id.into(),
607 PaginationRequest {
608 cursor: cursor.clone(),
609 results: NUM_RESULTS_PER_REQUEST,
610 direction: PageDirection::Forward,
611 },
612 )
613 .await?;
614
615 if response.results.is_empty() {
616 break;
617 }
618
619 contract_balances.extend(response.results.into_iter().map(
620 |ContractBalance {
621 contract: _,
622 amount,
623 asset_id,
624 }| (asset_id, amount),
625 ));
626 cursor = response.cursor;
627 }
628
629 Ok(contract_balances)
630 }
631
632 pub async fn get_transaction_by_id(&self, tx_id: &TxId) -> Result<Option<TransactionResponse>> {
633 Ok(self
634 .uncached_client()
635 .transaction(tx_id)
636 .await?
637 .map(Into::into))
638 }
639
640 pub async fn get_transactions(
641 &self,
642 request: PaginationRequest<String>,
643 ) -> Result<PaginatedResult<TransactionResponse, String>> {
644 let pr = self.uncached_client().transactions(request).await?;
645
646 Ok(PaginatedResult {
647 cursor: pr.cursor,
648 results: pr.results.into_iter().map(Into::into).collect(),
649 has_next_page: pr.has_next_page,
650 has_previous_page: pr.has_previous_page,
651 })
652 }
653
654 pub async fn get_transactions_by_owner(
656 &self,
657 owner: &Bech32Address,
658 request: PaginationRequest<String>,
659 ) -> Result<PaginatedResult<TransactionResponse, String>> {
660 let pr = self
661 .uncached_client()
662 .transactions_by_owner(&owner.into(), request)
663 .await?;
664
665 Ok(PaginatedResult {
666 cursor: pr.cursor,
667 results: pr.results.into_iter().map(Into::into).collect(),
668 has_next_page: pr.has_next_page,
669 has_previous_page: pr.has_previous_page,
670 })
671 }
672
673 pub async fn latest_block_height(&self) -> Result<u32> {
674 Ok(self.chain_info().await?.latest_block.header.height)
675 }
676
677 pub async fn latest_block_time(&self) -> Result<Option<DateTime<Utc>>> {
678 Ok(self.chain_info().await?.latest_block.header.time)
679 }
680
681 pub async fn produce_blocks(
682 &self,
683 blocks_to_produce: u32,
684 start_time: Option<DateTime<Utc>>,
685 ) -> Result<u32> {
686 let start_time = start_time.map(|time| Tai64::from_unix(time.timestamp()).0);
687
688 Ok(self
689 .uncached_client()
690 .produce_blocks(blocks_to_produce, start_time)
691 .await?
692 .into())
693 }
694
695 pub async fn block(&self, block_id: &Bytes32) -> Result<Option<Block>> {
696 Ok(self
697 .uncached_client()
698 .block(block_id)
699 .await?
700 .map(Into::into))
701 }
702
703 pub async fn block_by_height(&self, height: BlockHeight) -> Result<Option<Block>> {
704 Ok(self
705 .uncached_client()
706 .block_by_height(height)
707 .await?
708 .map(Into::into))
709 }
710
711 pub async fn get_blocks(
713 &self,
714 request: PaginationRequest<String>,
715 ) -> Result<PaginatedResult<Block, String>> {
716 let pr = self.uncached_client().blocks(request).await?;
717
718 Ok(PaginatedResult {
719 cursor: pr.cursor,
720 results: pr.results.into_iter().map(Into::into).collect(),
721 has_next_page: pr.has_next_page,
722 has_previous_page: pr.has_previous_page,
723 })
724 }
725
726 pub async fn estimate_transaction_cost<T: Transaction>(
727 &self,
728 tx: T,
729 tolerance: Option<f64>,
730 block_horizon: Option<u32>,
731 ) -> Result<TransactionCost> {
732 let block_horizon = block_horizon.unwrap_or(DEFAULT_GAS_ESTIMATION_BLOCK_HORIZON);
733 let tolerance = tolerance.unwrap_or(DEFAULT_GAS_ESTIMATION_TOLERANCE);
734
735 let EstimateGasPrice { gas_price, .. } = self.estimate_gas_price(block_horizon).await?;
736 let tx_status = self.dry_run_opt(tx.clone(), false, None).await?;
737
738 let total_gas = Self::apply_tolerance(tx_status.total_gas(), tolerance);
739 let total_fee = Self::apply_tolerance(tx_status.total_fee(), tolerance);
740
741 let receipts = tx_status.take_receipts();
742
743 Ok(TransactionCost {
744 gas_price,
745 metered_bytes_size: tx.metered_bytes_size() as u64,
746 total_fee,
747 total_gas,
748 script_gas: Self::get_script_gas_used(&receipts),
749 })
750 }
751
752 fn apply_tolerance(value: u64, tolerance: f64) -> u64 {
753 (value as f64 * (1.0 + tolerance)).ceil() as u64
754 }
755
756 fn get_script_gas_used(receipts: &[Receipt]) -> u64 {
757 receipts
758 .iter()
759 .rfind(|r| matches!(r, Receipt::ScriptResult { .. }))
760 .map(|script_result| {
761 script_result
762 .gas_used()
763 .expect("could not retrieve gas used from ScriptResult")
764 })
765 .unwrap_or(0)
766 }
767
768 pub async fn get_messages(&self, from: &Bech32Address) -> Result<Vec<Message>> {
769 let mut messages = Vec::new();
770 let mut cursor = None;
771
772 loop {
773 let response = self
774 .uncached_client()
775 .messages(
776 Some(&from.into()),
777 PaginationRequest {
778 cursor: cursor.clone(),
779 results: NUM_RESULTS_PER_REQUEST,
780 direction: PageDirection::Forward,
781 },
782 )
783 .await?;
784
785 if response.results.is_empty() {
786 break;
787 }
788
789 messages.extend(response.results.into_iter().map(Into::into));
790 cursor = response.cursor;
791 }
792
793 Ok(messages)
794 }
795
796 pub async fn get_message_proof(
797 &self,
798 tx_id: &TxId,
799 nonce: &Nonce,
800 commit_block_id: Option<&Bytes32>,
801 commit_block_height: Option<u32>,
802 ) -> Result<MessageProof> {
803 self.uncached_client()
804 .message_proof(
805 tx_id,
806 nonce,
807 commit_block_id,
808 commit_block_height.map(Into::into),
809 )
810 .await
811 .map(Into::into)
812 .map_err(Into::into)
813 }
814
815 pub async fn is_user_account(&self, address: impl Into<Bytes32>) -> Result<bool> {
816 self.uncached_client()
817 .is_user_account(*address.into())
818 .await
819 }
820
821 pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
822 self.uncached_client_mut().set_retry_config(retry_config);
823
824 self
825 }
826
827 pub async fn contract_exists(&self, contract_id: &Bech32ContractId) -> Result<bool> {
828 Ok(self
829 .uncached_client()
830 .contract_exists(&contract_id.into())
831 .await?)
832 }
833
834 fn uncached_client(&self) -> &RetryableClient {
835 self.cached_client.inner()
836 }
837
838 fn uncached_client_mut(&mut self) -> &mut RetryableClient {
839 self.cached_client.inner_mut()
840 }
841}
842
843#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
844impl DryRunner for Provider {
845 async fn dry_run(&self, tx: FuelTransaction) -> Result<DryRun> {
846 let [tx_execution_status] = self
847 .uncached_client()
848 .dry_run_opt(&vec![tx], Some(false), Some(0))
849 .await?
850 .try_into()
851 .expect("should have only one element");
852
853 let receipts = tx_execution_status.result.receipts();
854 let script_gas = Self::get_script_gas_used(receipts);
855
856 let variable_outputs = receipts
857 .iter()
858 .filter(
859 |receipt| matches!(receipt, Receipt::TransferOut { amount, .. } if *amount != 0),
860 )
861 .count();
862
863 let succeeded = matches!(
864 tx_execution_status.result,
865 TransactionExecutionResult::Success { .. }
866 );
867
868 let dry_run = DryRun {
869 succeeded,
870 script_gas,
871 variable_outputs,
872 };
873
874 Ok(dry_run)
875 }
876
877 async fn estimate_gas_price(&self, block_horizon: u32) -> Result<u64> {
878 Ok(self.estimate_gas_price(block_horizon).await?.gas_price)
879 }
880
881 async fn consensus_parameters(&self) -> Result<ConsensusParameters> {
882 Provider::consensus_parameters(self).await
883 }
884
885 async fn estimate_predicates(
886 &self,
887 tx: &FuelTransaction,
888 _latest_chain_executor_version: Option<u32>,
889 ) -> Result<FuelTransaction> {
890 Ok(self.uncached_client().estimate_predicates(tx).await?)
891 }
892}