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
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)]
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: 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)]
94// ANCHOR: resource_filter
95pub 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}
102// ANCHOR_END: resource_filter
103
104impl 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/// 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    /// Returns the underlying uncached client.
136    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    /// Connects to an existing node at the given address.
153    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    /// Sends a transaction to the underlying Provider's client.
184    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    /// Similar to `send_transaction_and_await_commit`,
217    /// but collect all the status received until a final one and return them.
218    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        // Process stream items until we get a final status
240        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        // Handle cache updates for failures
253        #[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    /// Gets all unspent coins owned by address `from`, with asset ID `asset_id`.
483    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    /// Get some spendable coins of asset `asset_id` for address `from` that add up at least to
535    /// amount `amount`. The returned coins (UTXOs) are actual coins that can be spent. The number
536    /// of coins (UXTOs) is optimized to prevent dust accumulation.
537    #[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    /// Get some spendable coins of asset `asset_id` for address `from` that add up at least to
543    /// amount `amount`. The returned coins (UTXOs) are actual coins that can be spent. The number
544    /// of coins (UXTOs) is optimized to prevent dust accumulation.
545    /// Coins that were recently submitted inside a tx will be ignored from the results.
546    #[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    /// Get the balance of all spendable coins `asset_id` for address `address`. This is different
592    /// from getting coins because we are just returning a number (the sum of UTXOs amount) instead
593    /// of the UTXOs.
594    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    /// Get the balance of all spendable coins `asset_id` for contract with id `contract_id`.
602    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    /// Get all the spendable balances of all assets for address `address`. This is different from
614    /// getting the coins because we are only returning the numbers (the sum of UTXOs coins amount
615    /// for each asset id) and not the UTXOs coins themselves
616    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    /// Get all balances of all assets for the contract with id `contract_id`.
663    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    // Get transaction(s) by owner
723    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    // - Get block(s)
780    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}