fuels_accounts/
provider.rs

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)]
62// ANCHOR: transaction_cost
63pub 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}
70// ANCHOR_END: transaction_cost
71
72pub(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)]
94// ANCHOR: resource_filter
95pub 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}
102// ANCHOR_END: resource_filter
103
104impl 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/// Encapsulates common client operations in the SDK.
120/// Note that you may also use `client`, which is an instance
121/// of `FuelClient`, directly, which provides a broader API.
122#[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    /// Connects to an existing node at the given address.
148    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    /// Sends a transaction to the underlying Provider's client.
179    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    /// Gets all unspent coins owned by address `from`, with asset ID `asset_id`.
405    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    /// Get some spendable coins of asset `asset_id` for address `from` that add up at least to
457    /// amount `amount`. The returned coins (UTXOs) are actual coins that can be spent. The number
458    /// of coins (UXTOs) is optimized to prevent dust accumulation.
459    #[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    /// Get some spendable coins of asset `asset_id` for address `from` that add up at least to
465    /// amount `amount`. The returned coins (UTXOs) are actual coins that can be spent. The number
466    /// of coins (UXTOs) is optimized to prevent dust accumulation.
467    /// Coins that were recently submitted inside a tx will be ignored from the results.
468    #[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    /// Get the balance of all spendable coins `asset_id` for address `address`. This is different
514    /// from getting coins because we are just returning a number (the sum of UTXOs amount) instead
515    /// of the UTXOs.
516    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    /// Get the balance of all spendable coins `asset_id` for contract with id `contract_id`.
528    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    /// Get all the spendable balances of all assets for address `address`. This is different from
540    /// getting the coins because we are only returning the numbers (the sum of UTXOs coins amount
541    /// for each asset id) and not the UTXOs coins themselves
542    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    /// Get all balances of all assets for the contract with id `contract_id`.
595    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    // Get transaction(s) by owner
655    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    // - Get block(s)
712    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}