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