Skip to main content

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