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