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