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
11pub use cache::TtlConfig;
12use cache::{CachedClient, SystemClock};
13use chrono::{DateTime, Utc};
14use fuel_core_client::client::{
15 FuelClient,
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};
27
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 Address, BlockHeight, Bytes32, ContractId, DryRun, DryRunner, Nonce,
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 },
47};
48use futures::StreamExt;
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::{cache::CacheableRpcs, 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: u128,
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, u128, Option<u16>)> {
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: Address,
97 pub asset_id: Option<AssetId>,
98 pub amount: u128,
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
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 client(&self) -> &FuelClient {
137 self.cached_client.inner().client()
138 }
139
140 pub fn set_cache_ttl(&mut self, ttl: TtlConfig) {
141 self.cached_client.set_ttl(ttl);
142 }
143
144 pub async fn clear_cache(&self) {
145 self.cached_client.clear().await;
146 }
147
148 pub async fn healthy(&self) -> Result<bool> {
149 Ok(self.uncached_client().health().await?)
150 }
151
152 pub async fn connect(url: impl AsRef<str>) -> Result<Provider> {
154 Self::connect_with_fallbacks(&[url]).await
155 }
156
157 pub async fn connect_with_fallbacks(urls: &[impl AsRef<str>]) -> Result<Provider> {
158 let client = CachedClient::new(
159 RetryableClient::connect_with_fallbacks(urls, Default::default()).await?,
160 TtlConfig::default(),
161 SystemClock,
162 );
163
164 Ok(Self {
165 cached_client: client,
166 #[cfg(feature = "coin-cache")]
167 coins_cache: Default::default(),
168 })
169 }
170
171 pub fn url(&self) -> &str {
172 self.uncached_client().url()
173 }
174
175 pub async fn blob(&self, blob_id: BlobId) -> Result<Option<Blob>> {
176 Ok(self
177 .uncached_client()
178 .blob(blob_id.into())
179 .await?
180 .map(|blob| Blob::new(blob.bytecode)))
181 }
182
183 pub async fn blob_exists(&self, blob_id: BlobId) -> Result<bool> {
184 Ok(self.uncached_client().blob_exists(blob_id.into()).await?)
185 }
186
187 pub async fn send_transaction_and_await_commit<T: Transaction>(
189 &self,
190 tx: T,
191 ) -> Result<TxStatus> {
192 #[cfg(feature = "coin-cache")]
193 let base_asset_id = *self.consensus_parameters().await?.base_asset_id();
194
195 #[cfg(feature = "coin-cache")]
196 self.check_inputs_already_in_cache(&tx.used_coins(&base_asset_id))
197 .await?;
198
199 let tx = self.prepare_transaction_for_sending(tx).await?;
200 let tx_status = self
201 .uncached_client()
202 .submit_and_await_commit(&tx.clone().into())
203 .await?
204 .into();
205
206 #[cfg(feature = "coin-cache")]
207 if matches!(
208 tx_status,
209 TxStatus::SqueezedOut { .. } | TxStatus::Failure { .. }
210 ) {
211 self.coins_cache
212 .lock()
213 .await
214 .remove_items(tx.used_coins(&base_asset_id))
215 }
216
217 Ok(tx_status)
218 }
219
220 pub async fn send_transaction_and_await_status<T: Transaction>(
223 &self,
224 tx: T,
225 include_preconfirmation: bool,
226 ) -> Result<Vec<Result<TxStatus>>> {
227 #[cfg(feature = "coin-cache")]
228 let base_asset_id = *self.consensus_parameters().await?.base_asset_id();
229 #[cfg(feature = "coin-cache")]
230 let used_base_coins = tx.used_coins(&base_asset_id);
231
232 #[cfg(feature = "coin-cache")]
233 self.check_inputs_already_in_cache(&used_base_coins).await?;
234
235 let tx = self.prepare_transaction_for_sending(tx).await?.into();
236 let mut stream = self
237 .uncached_client()
238 .submit_and_await_status(&tx, include_preconfirmation)
239 .await?;
240
241 let mut statuses = Vec::new();
242
243 while let Some(status) = stream.next().await {
245 let tx_status = status.map(TxStatus::from).map_err(Into::into);
246
247 let is_final = tx_status.as_ref().ok().is_some_and(|s| s.is_final());
248
249 statuses.push(tx_status);
250
251 if is_final {
252 break;
253 }
254 }
255
256 #[cfg(feature = "coin-cache")]
258 if statuses.iter().any(|status| {
259 matches!(
260 &status,
261 Ok(TxStatus::SqueezedOut { .. }) | Ok(TxStatus::Failure { .. }),
262 )
263 }) {
264 self.coins_cache.lock().await.remove_items(used_base_coins);
265 }
266
267 Ok(statuses)
268 }
269
270 async fn prepare_transaction_for_sending<T: Transaction>(&self, mut tx: T) -> Result<T> {
271 let consensus_parameters = self.consensus_parameters().await?;
272 tx.precompute(&consensus_parameters.chain_id())?;
273
274 let chain_info = self.chain_info().await?;
275 let Header {
276 height: latest_block_height,
277 state_transition_bytecode_version: latest_chain_executor_version,
278 ..
279 } = chain_info.latest_block.header;
280
281 if tx.is_using_predicates() {
282 tx.estimate_predicates(self, Some(latest_chain_executor_version))
283 .await?;
284 tx.clone()
285 .validate_predicates(&consensus_parameters, latest_block_height)?;
286 }
287
288 Ok(tx)
289 }
290
291 pub async fn send_transaction<T: Transaction>(&self, tx: T) -> Result<TxId> {
292 let tx = self.prepare_transaction_for_sending(tx).await?;
293 self.submit(tx).await
294 }
295
296 pub async fn await_transaction_commit<T: Transaction>(&self, id: TxId) -> Result<TxStatus> {
297 Ok(self
298 .uncached_client()
299 .await_transaction_commit(&id)
300 .await?
301 .into())
302 }
303
304 #[cfg(not(feature = "coin-cache"))]
305 async fn submit<T: Transaction>(&self, tx: T) -> Result<TxId> {
306 Ok(self.uncached_client().submit(&tx.into()).await?)
307 }
308
309 #[cfg(feature = "coin-cache")]
310 async fn find_in_cache<'a>(
311 &self,
312 coin_ids: impl IntoIterator<Item = (&'a (Address, AssetId), &'a Vec<CoinTypeId>)>,
313 ) -> Option<((Address, AssetId), CoinTypeId)> {
314 let mut locked_cache = self.coins_cache.lock().await;
315
316 for (key, ids) in coin_ids {
317 let items = locked_cache.get_active(key);
318
319 if items.is_empty() {
320 continue;
321 }
322
323 for id in ids {
324 if items.contains(id) {
325 return Some((*key, id.clone()));
326 }
327 }
328 }
329
330 None
331 }
332
333 #[cfg(feature = "coin-cache")]
334 async fn check_inputs_already_in_cache<'a>(
335 &self,
336 coin_ids: impl IntoIterator<Item = (&'a (Address, AssetId), &'a Vec<CoinTypeId>)>,
337 ) -> Result<()> {
338 use fuels_core::types::errors::{Error, transaction};
339
340 if let Some(((addr, asset_id), coin_type_id)) = self.find_in_cache(coin_ids).await {
341 let msg = match coin_type_id {
342 CoinTypeId::UtxoId(utxo_id) => format!("coin with utxo_id: `{utxo_id:x}`"),
343 CoinTypeId::Nonce(nonce) => format!("message with nonce: `{nonce}`"),
344 };
345 Err(Error::Transaction(transaction::Reason::Validation(
346 format!(
347 "{msg} was submitted recently in a transaction - attempting to spend it again will result in an error. Wallet address: `{addr}`, asset id: `{asset_id}`"
348 ),
349 )))
350 } else {
351 Ok(())
352 }
353 }
354
355 #[cfg(feature = "coin-cache")]
356 async fn submit<T: Transaction>(&self, tx: T) -> Result<TxId> {
357 let consensus_parameters = self.consensus_parameters().await?;
358 let base_asset_id = consensus_parameters.base_asset_id();
359
360 let used_utxos = tx.used_coins(base_asset_id);
361 self.check_inputs_already_in_cache(&used_utxos).await?;
362
363 let tx_id = self.uncached_client().submit(&tx.into()).await?;
364 self.coins_cache.lock().await.insert_multiple(used_utxos);
365
366 Ok(tx_id)
367 }
368
369 pub async fn tx_status(&self, tx_id: &TxId) -> Result<TxStatus> {
370 Ok(self
371 .uncached_client()
372 .transaction_status(tx_id)
373 .await?
374 .into())
375 }
376
377 pub async fn subscribe_transaction_status<'a>(
378 &'a self,
379 tx_id: &'a TxId,
380 include_preconfirmation: bool,
381 ) -> Result<impl futures::Stream<Item = Result<TxStatus>> + 'a> {
382 let stream = self
383 .uncached_client()
384 .subscribe_transaction_status(tx_id, include_preconfirmation)
385 .await?;
386
387 Ok(stream.map(|status| status.map(Into::into).map_err(Into::into)))
388 }
389
390 pub async fn chain_info(&self) -> Result<ChainInfo> {
391 Ok(self.uncached_client().chain_info().await?.into())
392 }
393
394 pub async fn consensus_parameters(&self) -> Result<ConsensusParameters> {
395 self.cached_client.consensus_parameters().await
396 }
397
398 pub async fn node_info(&self) -> Result<NodeInfo> {
399 Ok(self.cached_client.node_info().await?.into())
400 }
401
402 pub async fn latest_gas_price(&self) -> Result<LatestGasPrice> {
403 Ok(self.uncached_client().latest_gas_price().await?)
404 }
405
406 pub async fn estimate_gas_price(&self, block_horizon: u32) -> Result<EstimateGasPrice> {
407 Ok(self
408 .uncached_client()
409 .estimate_gas_price(block_horizon)
410 .await?)
411 }
412
413 pub async fn dry_run(&self, tx: impl Transaction) -> Result<TxStatus> {
414 let [tx_status] = self
415 .uncached_client()
416 .dry_run(Transactions::new().insert(tx).as_slice())
417 .await?
418 .into_iter()
419 .map(Into::into)
420 .collect::<Vec<_>>()
421 .try_into()
422 .expect("should have only one element");
423
424 Ok(tx_status)
425 }
426
427 pub async fn dry_run_multiple(
428 &self,
429 transactions: Transactions,
430 ) -> Result<Vec<(TxId, TxStatus)>> {
431 Ok(self
432 .uncached_client()
433 .dry_run(transactions.as_slice())
434 .await?
435 .into_iter()
436 .map(|execution_status| (execution_status.id, execution_status.into()))
437 .collect())
438 }
439
440 pub async fn dry_run_opt(
441 &self,
442 tx: impl Transaction,
443 utxo_validation: bool,
444 gas_price: Option<u64>,
445 at_height: Option<BlockHeight>,
446 ) -> Result<TxStatus> {
447 let [tx_status] = self
448 .uncached_client()
449 .dry_run_opt(
450 Transactions::new().insert(tx).as_slice(),
451 Some(utxo_validation),
452 gas_price,
453 at_height,
454 )
455 .await?
456 .into_iter()
457 .map(Into::into)
458 .collect::<Vec<_>>()
459 .try_into()
460 .expect("should have only one element");
461
462 Ok(tx_status)
463 }
464
465 pub async fn dry_run_opt_multiple(
466 &self,
467 transactions: Transactions,
468 utxo_validation: bool,
469 gas_price: Option<u64>,
470 at_height: Option<BlockHeight>,
471 ) -> Result<Vec<(TxId, TxStatus)>> {
472 Ok(self
473 .uncached_client()
474 .dry_run_opt(
475 transactions.as_slice(),
476 Some(utxo_validation),
477 gas_price,
478 at_height,
479 )
480 .await?
481 .into_iter()
482 .map(|execution_status| (execution_status.id, execution_status.into()))
483 .collect())
484 }
485
486 pub async fn get_coins(&self, from: &Address, asset_id: AssetId) -> Result<Vec<Coin>> {
488 let mut coins: Vec<Coin> = vec![];
489 let mut cursor = None;
490
491 loop {
492 let response = self
493 .uncached_client()
494 .coins(
495 from,
496 Some(&asset_id),
497 PaginationRequest {
498 cursor: cursor.clone(),
499 results: NUM_RESULTS_PER_REQUEST,
500 direction: PageDirection::Forward,
501 },
502 )
503 .await?;
504
505 if response.results.is_empty() {
506 break;
507 }
508
509 coins.extend(response.results.into_iter().map(Into::into));
510 cursor = response.cursor;
511 }
512
513 Ok(coins)
514 }
515
516 async fn request_coins_to_spend(&self, filter: ResourceFilter) -> Result<Vec<CoinType>> {
517 let queries = filter.resource_queries();
518
519 let consensus_parameters = self.consensus_parameters().await?;
520 let base_asset_id = *consensus_parameters.base_asset_id();
521
522 let res = self
523 .uncached_client()
524 .coins_to_spend(
525 &filter.owner(),
526 queries.spend_query(base_asset_id),
527 queries.exclusion_query(),
528 )
529 .await?
530 .into_iter()
531 .flatten()
532 .map(CoinType::from)
533 .collect();
534
535 Ok(res)
536 }
537
538 #[cfg(not(feature = "coin-cache"))]
542 pub async fn get_spendable_resources(&self, filter: ResourceFilter) -> Result<Vec<CoinType>> {
543 self.request_coins_to_spend(filter).await
544 }
545
546 #[cfg(feature = "coin-cache")]
551 pub async fn get_spendable_resources(
552 &self,
553 mut filter: ResourceFilter,
554 ) -> Result<Vec<CoinType>> {
555 self.extend_filter_with_cached(&mut filter).await?;
556
557 self.request_coins_to_spend(filter).await
558 }
559
560 #[cfg(feature = "coin-cache")]
561 async fn extend_filter_with_cached(&self, filter: &mut ResourceFilter) -> Result<()> {
562 let consensus_parameters = self.consensus_parameters().await?;
563 let mut cache = self.coins_cache.lock().await;
564 let asset_id = filter
565 .asset_id
566 .unwrap_or(*consensus_parameters.base_asset_id());
567 let used_coins = cache.get_active(&(filter.from, asset_id));
568
569 let excluded_utxos = used_coins
570 .iter()
571 .filter_map(|coin_id| match coin_id {
572 CoinTypeId::UtxoId(utxo_id) => Some(utxo_id),
573 _ => None,
574 })
575 .cloned()
576 .collect::<Vec<_>>();
577
578 let excluded_message_nonces = used_coins
579 .iter()
580 .filter_map(|coin_id| match coin_id {
581 CoinTypeId::Nonce(nonce) => Some(nonce),
582 _ => None,
583 })
584 .cloned()
585 .collect::<Vec<_>>();
586
587 filter.excluded_utxos.extend(excluded_utxos);
588 filter
589 .excluded_message_nonces
590 .extend(excluded_message_nonces);
591
592 Ok(())
593 }
594
595 pub async fn get_asset_balance(&self, address: &Address, asset_id: &AssetId) -> Result<u128> {
599 Ok(self
600 .uncached_client()
601 .balance(address, Some(asset_id))
602 .await?)
603 }
604
605 pub async fn get_contract_asset_balance(
607 &self,
608 contract_id: &ContractId,
609 asset_id: &AssetId,
610 ) -> Result<u64> {
611 Ok(self
612 .uncached_client()
613 .contract_balance(contract_id, Some(asset_id))
614 .await?)
615 }
616
617 pub async fn get_balances(&self, address: &Address) -> Result<HashMap<String, u128>> {
621 let mut balances = HashMap::new();
622
623 let mut register_balances = |results: Vec<_>| {
624 let pairs = results.into_iter().map(
625 |Balance {
626 owner: _,
627 amount,
628 asset_id,
629 }| (asset_id.to_string(), amount),
630 );
631 balances.extend(pairs);
632 };
633
634 let indexation_flags = self.cached_client.node_info().await?.indexation;
635 if indexation_flags.balances {
636 let mut cursor = None;
637 loop {
638 let pagination = PaginationRequest {
639 cursor: cursor.clone(),
640 results: NUM_RESULTS_PER_REQUEST,
641 direction: PageDirection::Forward,
642 };
643 let response = self.uncached_client().balances(address, pagination).await?;
644
645 if response.results.is_empty() {
646 break;
647 }
648
649 register_balances(response.results);
650 cursor = response.cursor;
651 }
652 } else {
653 let pagination = PaginationRequest {
654 cursor: None,
655 results: 9999,
656 direction: PageDirection::Forward,
657 };
658 let response = self.uncached_client().balances(address, pagination).await?;
659
660 register_balances(response.results)
661 }
662
663 Ok(balances)
664 }
665
666 pub async fn get_contract_balances(
668 &self,
669 contract_id: &ContractId,
670 ) -> Result<HashMap<AssetId, u64>> {
671 let mut contract_balances = HashMap::new();
672 let mut cursor = None;
673
674 loop {
675 let response = self
676 .uncached_client()
677 .contract_balances(
678 contract_id,
679 PaginationRequest {
680 cursor: cursor.clone(),
681 results: NUM_RESULTS_PER_REQUEST,
682 direction: PageDirection::Forward,
683 },
684 )
685 .await?;
686
687 if response.results.is_empty() {
688 break;
689 }
690
691 contract_balances.extend(response.results.into_iter().map(
692 |ContractBalance {
693 contract: _,
694 amount,
695 asset_id,
696 }| (asset_id, amount),
697 ));
698 cursor = response.cursor;
699 }
700
701 Ok(contract_balances)
702 }
703
704 pub async fn get_transaction_by_id(&self, tx_id: &TxId) -> Result<Option<TransactionResponse>> {
705 Ok(self
706 .uncached_client()
707 .transaction(tx_id)
708 .await?
709 .map(Into::into))
710 }
711
712 pub async fn get_transactions(
713 &self,
714 request: PaginationRequest<String>,
715 ) -> Result<PaginatedResult<TransactionResponse, String>> {
716 let pr = self.uncached_client().transactions(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 get_transactions_by_owner(
728 &self,
729 owner: &Address,
730 request: PaginationRequest<String>,
731 ) -> Result<PaginatedResult<TransactionResponse, String>> {
732 let pr = self
733 .uncached_client()
734 .transactions_by_owner(owner, request)
735 .await?;
736
737 Ok(PaginatedResult {
738 cursor: pr.cursor,
739 results: pr.results.into_iter().map(Into::into).collect(),
740 has_next_page: pr.has_next_page,
741 has_previous_page: pr.has_previous_page,
742 })
743 }
744
745 pub async fn latest_block_height(&self) -> Result<u32> {
746 Ok(self.chain_info().await?.latest_block.header.height)
747 }
748
749 pub async fn latest_block_time(&self) -> Result<Option<DateTime<Utc>>> {
750 Ok(self.chain_info().await?.latest_block.header.time)
751 }
752
753 pub async fn produce_blocks(
754 &self,
755 blocks_to_produce: u32,
756 start_time: Option<DateTime<Utc>>,
757 ) -> Result<u32> {
758 let start_time = start_time.map(|time| Tai64::from_unix(time.timestamp()).0);
759
760 Ok(self
761 .uncached_client()
762 .produce_blocks(blocks_to_produce, start_time)
763 .await?
764 .into())
765 }
766
767 pub async fn block(&self, block_id: &Bytes32) -> Result<Option<Block>> {
768 Ok(self
769 .uncached_client()
770 .block(block_id)
771 .await?
772 .map(Into::into))
773 }
774
775 pub async fn block_by_height(&self, height: BlockHeight) -> Result<Option<Block>> {
776 Ok(self
777 .uncached_client()
778 .block_by_height(height)
779 .await?
780 .map(Into::into))
781 }
782
783 pub async fn get_blocks(
785 &self,
786 request: PaginationRequest<String>,
787 ) -> Result<PaginatedResult<Block, String>> {
788 let pr = self.uncached_client().blocks(request).await?;
789
790 Ok(PaginatedResult {
791 cursor: pr.cursor,
792 results: pr.results.into_iter().map(Into::into).collect(),
793 has_next_page: pr.has_next_page,
794 has_previous_page: pr.has_previous_page,
795 })
796 }
797
798 pub async fn estimate_transaction_cost<T: Transaction>(
799 &self,
800 tx: T,
801 tolerance: Option<f64>,
802 block_horizon: Option<u32>,
803 ) -> Result<TransactionCost> {
804 let block_horizon = block_horizon.unwrap_or(DEFAULT_GAS_ESTIMATION_BLOCK_HORIZON);
805 let tolerance = tolerance.unwrap_or(DEFAULT_GAS_ESTIMATION_TOLERANCE);
806
807 let EstimateGasPrice { gas_price, .. } = self.estimate_gas_price(block_horizon).await?;
808 let tx_status = self.dry_run_opt(tx.clone(), false, None, None).await?;
809
810 let total_gas = Self::apply_tolerance(tx_status.total_gas(), tolerance);
811 let total_fee = Self::apply_tolerance(tx_status.total_fee(), tolerance);
812
813 let receipts = tx_status.take_receipts();
814
815 Ok(TransactionCost {
816 gas_price,
817 metered_bytes_size: tx.metered_bytes_size() as u64,
818 total_fee,
819 total_gas,
820 script_gas: Self::get_script_gas_used(&receipts),
821 })
822 }
823
824 fn apply_tolerance(value: u64, tolerance: f64) -> u64 {
825 (value as f64 * (1.0 + tolerance)).ceil() as u64
826 }
827
828 fn get_script_gas_used(receipts: &[Receipt]) -> u64 {
829 receipts
830 .iter()
831 .rfind(|r| matches!(r, Receipt::ScriptResult { .. }))
832 .map(|script_result| {
833 script_result
834 .gas_used()
835 .expect("could not retrieve gas used from ScriptResult")
836 })
837 .unwrap_or(0)
838 }
839
840 pub async fn get_messages(&self, from: &Address) -> Result<Vec<Message>> {
841 let mut messages = Vec::new();
842 let mut cursor = None;
843
844 loop {
845 let response = self
846 .uncached_client()
847 .messages(
848 Some(from),
849 PaginationRequest {
850 cursor: cursor.clone(),
851 results: NUM_RESULTS_PER_REQUEST,
852 direction: PageDirection::Forward,
853 },
854 )
855 .await?;
856
857 if response.results.is_empty() {
858 break;
859 }
860
861 messages.extend(response.results.into_iter().map(Into::into));
862 cursor = response.cursor;
863 }
864
865 Ok(messages)
866 }
867
868 pub async fn get_message_proof(
869 &self,
870 tx_id: &TxId,
871 nonce: &Nonce,
872 commit_block_id: Option<&Bytes32>,
873 commit_block_height: Option<u32>,
874 ) -> Result<MessageProof> {
875 self.uncached_client()
876 .message_proof(
877 tx_id,
878 nonce,
879 commit_block_id,
880 commit_block_height.map(Into::into),
881 )
882 .await
883 .map(Into::into)
884 .map_err(Into::into)
885 }
886
887 pub async fn is_user_account(&self, address: impl Into<Bytes32>) -> Result<bool> {
888 self.uncached_client()
889 .is_user_account(*address.into())
890 .await
891 }
892
893 pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
894 self.uncached_client_mut().set_retry_config(retry_config);
895
896 self
897 }
898
899 pub async fn contract_exists(&self, contract_id: &ContractId) -> Result<bool> {
900 Ok(self.uncached_client().contract_exists(contract_id).await?)
901 }
902
903 fn uncached_client(&self) -> &RetryableClient {
904 self.cached_client.inner()
905 }
906
907 fn uncached_client_mut(&mut self) -> &mut RetryableClient {
908 self.cached_client.inner_mut()
909 }
910}
911
912#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
913impl DryRunner for Provider {
914 async fn dry_run(&self, tx: FuelTransaction) -> Result<DryRun> {
915 let [tx_execution_status] = self
916 .uncached_client()
917 .dry_run_opt(&[tx], Some(false), Some(0), None)
918 .await?
919 .try_into()
920 .expect("should have only one element");
921
922 let receipts = tx_execution_status.result.receipts();
923 let script_gas = Self::get_script_gas_used(receipts);
924
925 let variable_outputs = receipts
926 .iter()
927 .filter(
928 |receipt| matches!(receipt, Receipt::TransferOut { amount, .. } if *amount != 0),
929 )
930 .count();
931
932 let succeeded = matches!(
933 tx_execution_status.result,
934 TransactionExecutionResult::Success { .. }
935 );
936
937 let dry_run = DryRun {
938 succeeded,
939 script_gas,
940 variable_outputs,
941 };
942
943 Ok(dry_run)
944 }
945
946 async fn estimate_gas_price(&self, block_horizon: u32) -> Result<u64> {
947 Ok(self.estimate_gas_price(block_horizon).await?.gas_price)
948 }
949
950 async fn consensus_parameters(&self) -> Result<ConsensusParameters> {
951 Provider::consensus_parameters(self).await
952 }
953
954 async fn estimate_predicates(
955 &self,
956 tx: &FuelTransaction,
957 _latest_chain_executor_version: Option<u32>,
958 ) -> Result<FuelTransaction> {
959 Ok(self.uncached_client().estimate_predicates(tx).await?)
960 }
961}