fuels_accounts/
provider.rs

1#[cfg(feature = "coin-cache")]
2use std::sync::Arc;
3use std::{collections::HashMap, fmt::Debug, net::SocketAddr};
4
5mod cache;
6mod retry_util;
7mod retryable_client;
8mod supported_fuel_core_version;
9mod supported_versions;
10
11use crate::provider::cache::CacheableRpcs;
12pub use cache::TtlConfig;
13use cache::{CachedClient, SystemClock};
14use chrono::{DateTime, Utc};
15use fuel_core_client::client::{
16    pagination::{PageDirection, PaginatedResult, PaginationRequest},
17    types::{
18        balance::Balance,
19        contract::ContractBalance,
20        gas_price::{EstimateGasPrice, LatestGasPrice},
21    },
22};
23use fuel_core_types::services::executor::TransactionExecutionResult;
24use fuel_tx::{
25    AssetId, ConsensusParameters, Receipt, Transaction as FuelTransaction, TxId, UtxoId,
26};
27use fuel_types::{Address, BlockHeight, Bytes32, Nonce};
28#[cfg(feature = "coin-cache")]
29use fuels_core::types::coin_type_id::CoinTypeId;
30use fuels_core::{
31    constants::{DEFAULT_GAS_ESTIMATION_BLOCK_HORIZON, DEFAULT_GAS_ESTIMATION_TOLERANCE},
32    types::{
33        bech32::{Bech32Address, Bech32ContractId},
34        block::{Block, Header},
35        chain_info::ChainInfo,
36        coin::Coin,
37        coin_type::CoinType,
38        errors::Result,
39        message::Message,
40        message_proof::MessageProof,
41        node_info::NodeInfo,
42        transaction::{Transaction, Transactions},
43        transaction_builders::{Blob, BlobId},
44        transaction_response::TransactionResponse,
45        tx_status::TxStatus,
46        DryRun, DryRunner,
47    },
48};
49pub use retry_util::{Backoff, RetryConfig};
50pub use supported_fuel_core_version::SUPPORTED_FUEL_CORE_VERSION;
51use tai64::Tai64;
52#[cfg(feature = "coin-cache")]
53use tokio::sync::Mutex;
54
55#[cfg(feature = "coin-cache")]
56use crate::coin_cache::CoinsCache;
57use crate::provider::retryable_client::RetryableClient;
58
59const NUM_RESULTS_PER_REQUEST: i32 = 100;
60
61#[derive(Debug, Clone, PartialEq)]
62// ANCHOR: transaction_cost
63pub struct TransactionCost {
64    pub gas_price: u64,
65    pub gas_used: u64,
66    pub metered_bytes_size: u64,
67    pub total_fee: u64,
68}
69// ANCHOR_END: transaction_cost
70
71pub(crate) struct ResourceQueries {
72    utxos: Vec<UtxoId>,
73    messages: Vec<Nonce>,
74    asset_id: Option<AssetId>,
75    amount: u64,
76}
77
78impl ResourceQueries {
79    pub fn exclusion_query(&self) -> Option<(Vec<UtxoId>, Vec<Nonce>)> {
80        if self.utxos.is_empty() && self.messages.is_empty() {
81            return None;
82        }
83
84        Some((self.utxos.clone(), self.messages.clone()))
85    }
86
87    pub fn spend_query(&self, base_asset_id: AssetId) -> Vec<(AssetId, u64, Option<u32>)> {
88        vec![(self.asset_id.unwrap_or(base_asset_id), self.amount, None)]
89    }
90}
91
92#[derive(Default)]
93// ANCHOR: resource_filter
94pub struct ResourceFilter {
95    pub from: Bech32Address,
96    pub asset_id: Option<AssetId>,
97    pub amount: u64,
98    pub excluded_utxos: Vec<UtxoId>,
99    pub excluded_message_nonces: Vec<Nonce>,
100}
101// ANCHOR_END: resource_filter
102
103impl ResourceFilter {
104    pub fn owner(&self) -> Address {
105        (&self.from).into()
106    }
107
108    pub(crate) fn resource_queries(&self) -> ResourceQueries {
109        ResourceQueries {
110            utxos: self.excluded_utxos.clone(),
111            messages: self.excluded_message_nonces.clone(),
112            asset_id: self.asset_id,
113            amount: self.amount,
114        }
115    }
116}
117
118/// Encapsulates common client operations in the SDK.
119/// Note that you may also use `client`, which is an instance
120/// of `FuelClient`, directly, which provides a broader API.
121#[derive(Debug, Clone)]
122pub struct Provider {
123    cached_client: CachedClient<RetryableClient>,
124    #[cfg(feature = "coin-cache")]
125    coins_cache: Arc<Mutex<CoinsCache>>,
126}
127
128impl Provider {
129    pub async fn from(addr: impl Into<SocketAddr>) -> Result<Self> {
130        let addr = addr.into();
131        Self::connect(format!("http://{addr}")).await
132    }
133
134    pub fn set_cache_ttl(&mut self, ttl: TtlConfig) {
135        self.cached_client.set_ttl(ttl);
136    }
137
138    pub async fn clear_cache(&self) {
139        self.cached_client.clear().await;
140    }
141
142    pub async fn healthy(&self) -> Result<bool> {
143        Ok(self.uncached_client().health().await?)
144    }
145
146    /// Connects to an existing node at the given address.
147    pub async fn connect(url: impl AsRef<str>) -> Result<Provider> {
148        let client = CachedClient::new(
149            RetryableClient::connect(&url, Default::default()).await?,
150            TtlConfig::default(),
151            SystemClock,
152        );
153
154        Ok(Self {
155            cached_client: client,
156            #[cfg(feature = "coin-cache")]
157            coins_cache: Default::default(),
158        })
159    }
160
161    pub fn url(&self) -> &str {
162        self.uncached_client().url()
163    }
164
165    pub async fn blob(&self, blob_id: BlobId) -> Result<Option<Blob>> {
166        Ok(self
167            .uncached_client()
168            .blob(blob_id.into())
169            .await?
170            .map(|blob| Blob::new(blob.bytecode)))
171    }
172
173    pub async fn blob_exists(&self, blob_id: BlobId) -> Result<bool> {
174        Ok(self.uncached_client().blob_exists(blob_id.into()).await?)
175    }
176
177    /// Sends a transaction to the underlying Provider's client.
178    pub async fn send_transaction_and_await_commit<T: Transaction>(
179        &self,
180        tx: T,
181    ) -> Result<TxStatus> {
182        #[cfg(feature = "coin-cache")]
183        let base_asset_id = *self.consensus_parameters().await?.base_asset_id();
184
185        #[cfg(feature = "coin-cache")]
186        self.check_inputs_already_in_cache(&tx.used_coins(&base_asset_id))
187            .await?;
188
189        let tx = self.prepare_transaction_for_sending(tx).await?;
190        let tx_status = self
191            .uncached_client()
192            .submit_and_await_commit(&tx.clone().into())
193            .await?
194            .into();
195
196        #[cfg(feature = "coin-cache")]
197        if matches!(
198            tx_status,
199            TxStatus::SqueezedOut { .. } | TxStatus::Revert { .. }
200        ) {
201            self.coins_cache
202                .lock()
203                .await
204                .remove_items(tx.used_coins(&base_asset_id))
205        }
206
207        Ok(tx_status)
208    }
209
210    async fn prepare_transaction_for_sending<T: Transaction>(&self, mut tx: T) -> Result<T> {
211        let consensus_parameters = self.consensus_parameters().await?;
212        tx.precompute(&consensus_parameters.chain_id())?;
213
214        let chain_info = self.chain_info().await?;
215        let Header {
216            height: latest_block_height,
217            state_transition_bytecode_version: latest_chain_executor_version,
218            ..
219        } = chain_info.latest_block.header;
220
221        if tx.is_using_predicates() {
222            tx.estimate_predicates(self, Some(latest_chain_executor_version))
223                .await?;
224            tx.clone()
225                .validate_predicates(&consensus_parameters, latest_block_height)?;
226        }
227
228        self.validate_transaction(tx.clone()).await?;
229
230        Ok(tx)
231    }
232
233    pub async fn send_transaction<T: Transaction>(&self, tx: T) -> Result<TxId> {
234        let tx = self.prepare_transaction_for_sending(tx).await?;
235        self.submit(tx).await
236    }
237
238    pub async fn await_transaction_commit<T: Transaction>(&self, id: TxId) -> Result<TxStatus> {
239        Ok(self
240            .uncached_client()
241            .await_transaction_commit(&id)
242            .await?
243            .into())
244    }
245
246    async fn validate_transaction<T: Transaction>(&self, tx: T) -> Result<()> {
247        let tolerance = 0.0;
248        let TransactionCost { gas_used, .. } = self
249            .estimate_transaction_cost(tx.clone(), Some(tolerance), None)
250            .await?;
251
252        tx.validate_gas(gas_used)?;
253
254        Ok(())
255    }
256
257    #[cfg(not(feature = "coin-cache"))]
258    async fn submit<T: Transaction>(&self, tx: T) -> Result<TxId> {
259        Ok(self.uncached_client().submit(&tx.into()).await?)
260    }
261
262    #[cfg(feature = "coin-cache")]
263    async fn find_in_cache<'a>(
264        &self,
265        coin_ids: impl IntoIterator<Item = (&'a (Bech32Address, AssetId), &'a Vec<CoinTypeId>)>,
266    ) -> Option<((Bech32Address, AssetId), CoinTypeId)> {
267        let mut locked_cache = self.coins_cache.lock().await;
268
269        for (key, ids) in coin_ids {
270            let items = locked_cache.get_active(key);
271
272            if items.is_empty() {
273                continue;
274            }
275
276            for id in ids {
277                if items.contains(id) {
278                    return Some((key.clone(), id.clone()));
279                }
280            }
281        }
282
283        None
284    }
285
286    #[cfg(feature = "coin-cache")]
287    async fn check_inputs_already_in_cache<'a>(
288        &self,
289        coin_ids: impl IntoIterator<Item = (&'a (Bech32Address, AssetId), &'a Vec<CoinTypeId>)>,
290    ) -> Result<()> {
291        use fuels_core::types::errors::{transaction, Error};
292
293        if let Some(((addr, asset_id), coin_type_id)) = self.find_in_cache(coin_ids).await {
294            let msg = match coin_type_id {
295                CoinTypeId::UtxoId(utxo_id) => format!("coin with utxo_id: `{utxo_id:x}`"),
296                CoinTypeId::Nonce(nonce) => format!("message with nonce: `{nonce}`"),
297            };
298            Err(Error::Transaction(transaction::Reason::Validation(
299                format!("{msg} was submitted recently in a transaction - attempting to spend it again will result in an error. Wallet address: `{addr}`, asset id: `{asset_id}`"),
300            )))
301        } else {
302            Ok(())
303        }
304    }
305
306    #[cfg(feature = "coin-cache")]
307    async fn submit<T: Transaction>(&self, tx: T) -> Result<TxId> {
308        let consensus_parameters = self.consensus_parameters().await?;
309        let base_asset_id = consensus_parameters.base_asset_id();
310
311        let used_utxos = tx.used_coins(base_asset_id);
312        self.check_inputs_already_in_cache(&used_utxos).await?;
313
314        let tx_id = self.uncached_client().submit(&tx.into()).await?;
315        self.coins_cache.lock().await.insert_multiple(used_utxos);
316
317        Ok(tx_id)
318    }
319
320    pub async fn tx_status(&self, tx_id: &TxId) -> Result<TxStatus> {
321        Ok(self
322            .uncached_client()
323            .transaction_status(tx_id)
324            .await?
325            .into())
326    }
327
328    pub async fn chain_info(&self) -> Result<ChainInfo> {
329        Ok(self.uncached_client().chain_info().await?.into())
330    }
331
332    pub async fn consensus_parameters(&self) -> Result<ConsensusParameters> {
333        self.cached_client.consensus_parameters().await
334    }
335
336    pub async fn node_info(&self) -> Result<NodeInfo> {
337        Ok(self.uncached_client().node_info().await?.into())
338    }
339
340    pub async fn latest_gas_price(&self) -> Result<LatestGasPrice> {
341        Ok(self.uncached_client().latest_gas_price().await?)
342    }
343
344    pub async fn estimate_gas_price(&self, block_horizon: u32) -> Result<EstimateGasPrice> {
345        Ok(self
346            .uncached_client()
347            .estimate_gas_price(block_horizon)
348            .await?)
349    }
350
351    pub async fn dry_run(&self, tx: impl Transaction) -> Result<TxStatus> {
352        let [tx_status] = self
353            .uncached_client()
354            .dry_run(Transactions::new().insert(tx).as_slice())
355            .await?
356            .into_iter()
357            .map(Into::into)
358            .collect::<Vec<_>>()
359            .try_into()
360            .expect("should have only one element");
361
362        Ok(tx_status)
363    }
364
365    pub async fn dry_run_multiple(
366        &self,
367        transactions: Transactions,
368    ) -> Result<Vec<(TxId, TxStatus)>> {
369        Ok(self
370            .uncached_client()
371            .dry_run(transactions.as_slice())
372            .await?
373            .into_iter()
374            .map(|execution_status| (execution_status.id, execution_status.into()))
375            .collect())
376    }
377
378    pub async fn dry_run_opt(
379        &self,
380        tx: impl Transaction,
381        utxo_validation: bool,
382        gas_price: Option<u64>,
383    ) -> Result<TxStatus> {
384        let [tx_status] = self
385            .uncached_client()
386            .dry_run_opt(
387                Transactions::new().insert(tx).as_slice(),
388                Some(utxo_validation),
389                gas_price,
390            )
391            .await?
392            .into_iter()
393            .map(Into::into)
394            .collect::<Vec<_>>()
395            .try_into()
396            .expect("should have only one element");
397
398        Ok(tx_status)
399    }
400
401    pub async fn dry_run_opt_multiple(
402        &self,
403        transactions: Transactions,
404        utxo_validation: bool,
405        gas_price: Option<u64>,
406    ) -> Result<Vec<(TxId, TxStatus)>> {
407        Ok(self
408            .uncached_client()
409            .dry_run_opt(transactions.as_slice(), Some(utxo_validation), gas_price)
410            .await?
411            .into_iter()
412            .map(|execution_status| (execution_status.id, execution_status.into()))
413            .collect())
414    }
415
416    /// Gets all unspent coins owned by address `from`, with asset ID `asset_id`.
417    pub async fn get_coins(&self, from: &Bech32Address, asset_id: AssetId) -> Result<Vec<Coin>> {
418        let mut coins: Vec<Coin> = vec![];
419        let mut cursor = None;
420
421        loop {
422            let response = self
423                .uncached_client()
424                .coins(
425                    &from.into(),
426                    Some(&asset_id),
427                    PaginationRequest {
428                        cursor: cursor.clone(),
429                        results: NUM_RESULTS_PER_REQUEST,
430                        direction: PageDirection::Forward,
431                    },
432                )
433                .await?;
434
435            if response.results.is_empty() {
436                break;
437            }
438
439            coins.extend(response.results.into_iter().map(Into::into));
440            cursor = response.cursor;
441        }
442
443        Ok(coins)
444    }
445
446    async fn request_coins_to_spend(&self, filter: ResourceFilter) -> Result<Vec<CoinType>> {
447        let queries = filter.resource_queries();
448
449        let consensus_parameters = self.consensus_parameters().await?;
450        let base_asset_id = *consensus_parameters.base_asset_id();
451
452        let res = self
453            .uncached_client()
454            .coins_to_spend(
455                &filter.owner(),
456                queries.spend_query(base_asset_id),
457                queries.exclusion_query(),
458            )
459            .await?
460            .into_iter()
461            .flatten()
462            .map(CoinType::from)
463            .collect();
464
465        Ok(res)
466    }
467
468    /// Get some spendable coins of asset `asset_id` for address `from` that add up at least to
469    /// amount `amount`. The returned coins (UTXOs) are actual coins that can be spent. The number
470    /// of coins (UXTOs) is optimized to prevent dust accumulation.
471    #[cfg(not(feature = "coin-cache"))]
472    pub async fn get_spendable_resources(&self, filter: ResourceFilter) -> Result<Vec<CoinType>> {
473        self.request_coins_to_spend(filter).await
474    }
475
476    /// Get some spendable coins of asset `asset_id` for address `from` that add up at least to
477    /// amount `amount`. The returned coins (UTXOs) are actual coins that can be spent. The number
478    /// of coins (UXTOs) is optimized to prevent dust accumulation.
479    /// Coins that were recently submitted inside a tx will be ignored from the results.
480    #[cfg(feature = "coin-cache")]
481    pub async fn get_spendable_resources(
482        &self,
483        mut filter: ResourceFilter,
484    ) -> Result<Vec<CoinType>> {
485        self.extend_filter_with_cached(&mut filter).await?;
486
487        self.request_coins_to_spend(filter).await
488    }
489
490    #[cfg(feature = "coin-cache")]
491    async fn extend_filter_with_cached(&self, filter: &mut ResourceFilter) -> Result<()> {
492        let consensus_parameters = self.consensus_parameters().await?;
493        let mut cache = self.coins_cache.lock().await;
494        let asset_id = filter
495            .asset_id
496            .unwrap_or(*consensus_parameters.base_asset_id());
497        let used_coins = cache.get_active(&(filter.from.clone(), asset_id));
498
499        let excluded_utxos = used_coins
500            .iter()
501            .filter_map(|coin_id| match coin_id {
502                CoinTypeId::UtxoId(utxo_id) => Some(utxo_id),
503                _ => None,
504            })
505            .cloned()
506            .collect::<Vec<_>>();
507
508        let excluded_message_nonces = used_coins
509            .iter()
510            .filter_map(|coin_id| match coin_id {
511                CoinTypeId::Nonce(nonce) => Some(nonce),
512                _ => None,
513            })
514            .cloned()
515            .collect::<Vec<_>>();
516
517        filter.excluded_utxos.extend(excluded_utxos);
518        filter
519            .excluded_message_nonces
520            .extend(excluded_message_nonces);
521
522        Ok(())
523    }
524
525    /// Get the balance of all spendable coins `asset_id` for address `address`. This is different
526    /// from getting coins because we are just returning a number (the sum of UTXOs amount) instead
527    /// of the UTXOs.
528    pub async fn get_asset_balance(
529        &self,
530        address: &Bech32Address,
531        asset_id: AssetId,
532    ) -> Result<u64> {
533        Ok(self
534            .uncached_client()
535            .balance(&address.into(), Some(&asset_id))
536            .await?)
537    }
538
539    /// Get the balance of all spendable coins `asset_id` for contract with id `contract_id`.
540    pub async fn get_contract_asset_balance(
541        &self,
542        contract_id: &Bech32ContractId,
543        asset_id: AssetId,
544    ) -> Result<u64> {
545        Ok(self
546            .uncached_client()
547            .contract_balance(&contract_id.into(), Some(&asset_id))
548            .await?)
549    }
550
551    /// Get all the spendable balances of all assets for address `address`. This is different from
552    /// getting the coins because we are only returning the numbers (the sum of UTXOs coins amount
553    /// for each asset id) and not the UTXOs coins themselves
554    pub async fn get_balances(&self, address: &Bech32Address) -> Result<HashMap<String, u128>> {
555        let mut balances = HashMap::new();
556        let mut cursor = None;
557
558        loop {
559            let response = self
560                .uncached_client()
561                .balances(
562                    &address.into(),
563                    PaginationRequest {
564                        cursor: cursor.clone(),
565                        results: NUM_RESULTS_PER_REQUEST,
566                        direction: PageDirection::Forward,
567                    },
568                )
569                .await?;
570
571            if response.results.is_empty() {
572                break;
573            }
574
575            balances.extend(response.results.into_iter().map(
576                |Balance {
577                     owner: _,
578                     amount,
579                     asset_id,
580                 }| (asset_id.to_string(), amount),
581            ));
582            cursor = response.cursor;
583        }
584
585        Ok(balances)
586    }
587
588    /// Get all balances of all assets for the contract with id `contract_id`.
589    pub async fn get_contract_balances(
590        &self,
591        contract_id: &Bech32ContractId,
592    ) -> Result<HashMap<AssetId, u64>> {
593        let mut contract_balances = HashMap::new();
594        let mut cursor = None;
595
596        loop {
597            let response = self
598                .uncached_client()
599                .contract_balances(
600                    &contract_id.into(),
601                    PaginationRequest {
602                        cursor: cursor.clone(),
603                        results: NUM_RESULTS_PER_REQUEST,
604                        direction: PageDirection::Forward,
605                    },
606                )
607                .await?;
608
609            if response.results.is_empty() {
610                break;
611            }
612
613            contract_balances.extend(response.results.into_iter().map(
614                |ContractBalance {
615                     contract: _,
616                     amount,
617                     asset_id,
618                 }| (asset_id, amount),
619            ));
620            cursor = response.cursor;
621        }
622
623        Ok(contract_balances)
624    }
625
626    pub async fn get_transaction_by_id(&self, tx_id: &TxId) -> Result<Option<TransactionResponse>> {
627        Ok(self
628            .uncached_client()
629            .transaction(tx_id)
630            .await?
631            .map(Into::into))
632    }
633
634    pub async fn get_transactions(
635        &self,
636        request: PaginationRequest<String>,
637    ) -> Result<PaginatedResult<TransactionResponse, String>> {
638        let pr = self.uncached_client().transactions(request).await?;
639
640        Ok(PaginatedResult {
641            cursor: pr.cursor,
642            results: pr.results.into_iter().map(Into::into).collect(),
643            has_next_page: pr.has_next_page,
644            has_previous_page: pr.has_previous_page,
645        })
646    }
647
648    // Get transaction(s) by owner
649    pub async fn get_transactions_by_owner(
650        &self,
651        owner: &Bech32Address,
652        request: PaginationRequest<String>,
653    ) -> Result<PaginatedResult<TransactionResponse, String>> {
654        let pr = self
655            .uncached_client()
656            .transactions_by_owner(&owner.into(), request)
657            .await?;
658
659        Ok(PaginatedResult {
660            cursor: pr.cursor,
661            results: pr.results.into_iter().map(Into::into).collect(),
662            has_next_page: pr.has_next_page,
663            has_previous_page: pr.has_previous_page,
664        })
665    }
666
667    pub async fn latest_block_height(&self) -> Result<u32> {
668        Ok(self.chain_info().await?.latest_block.header.height)
669    }
670
671    pub async fn latest_block_time(&self) -> Result<Option<DateTime<Utc>>> {
672        Ok(self.chain_info().await?.latest_block.header.time)
673    }
674
675    pub async fn produce_blocks(
676        &self,
677        blocks_to_produce: u32,
678        start_time: Option<DateTime<Utc>>,
679    ) -> Result<u32> {
680        let start_time = start_time.map(|time| Tai64::from_unix(time.timestamp()).0);
681
682        Ok(self
683            .uncached_client()
684            .produce_blocks(blocks_to_produce, start_time)
685            .await?
686            .into())
687    }
688
689    pub async fn block(&self, block_id: &Bytes32) -> Result<Option<Block>> {
690        Ok(self
691            .uncached_client()
692            .block(block_id)
693            .await?
694            .map(Into::into))
695    }
696
697    pub async fn block_by_height(&self, height: BlockHeight) -> Result<Option<Block>> {
698        Ok(self
699            .uncached_client()
700            .block_by_height(height)
701            .await?
702            .map(Into::into))
703    }
704
705    // - Get block(s)
706    pub async fn get_blocks(
707        &self,
708        request: PaginationRequest<String>,
709    ) -> Result<PaginatedResult<Block, String>> {
710        let pr = self.uncached_client().blocks(request).await?;
711
712        Ok(PaginatedResult {
713            cursor: pr.cursor,
714            results: pr.results.into_iter().map(Into::into).collect(),
715            has_next_page: pr.has_next_page,
716            has_previous_page: pr.has_previous_page,
717        })
718    }
719
720    pub async fn estimate_transaction_cost<T: Transaction>(
721        &self,
722        mut tx: T,
723        tolerance: Option<f64>,
724        block_horizon: Option<u32>,
725    ) -> Result<TransactionCost> {
726        let block_horizon = block_horizon.unwrap_or(DEFAULT_GAS_ESTIMATION_BLOCK_HORIZON);
727        let tolerance = tolerance.unwrap_or(DEFAULT_GAS_ESTIMATION_TOLERANCE);
728
729        let EstimateGasPrice { gas_price, .. } = self.estimate_gas_price(block_horizon).await?;
730
731        let gas_used = self
732            .get_gas_used_with_tolerance(tx.clone(), tolerance)
733            .await?;
734
735        if tx.is_using_predicates() {
736            tx.estimate_predicates(self, None).await?;
737        }
738
739        let transaction_fee = tx
740            .clone()
741            .fee_checked_from_tx(&self.consensus_parameters().await?, gas_price)
742            .expect("Error calculating TransactionFee");
743
744        Ok(TransactionCost {
745            gas_price,
746            gas_used,
747            metered_bytes_size: tx.metered_bytes_size() as u64,
748            total_fee: transaction_fee.max_fee(),
749        })
750    }
751
752    // Increase estimated gas by the provided tolerance
753    async fn get_gas_used_with_tolerance<T: Transaction>(
754        &self,
755        tx: T,
756        tolerance: f64,
757    ) -> Result<u64> {
758        let receipts = self.dry_run_opt(tx, false, None).await?.take_receipts();
759        let gas_used = self.get_script_gas_used(&receipts);
760
761        Ok((gas_used as f64 * (1.0 + tolerance)).ceil() as u64)
762    }
763
764    fn get_script_gas_used(&self, receipts: &[Receipt]) -> u64 {
765        receipts
766            .iter()
767            .rfind(|r| matches!(r, Receipt::ScriptResult { .. }))
768            .map(|script_result| {
769                script_result
770                    .gas_used()
771                    .expect("could not retrieve gas used from ScriptResult")
772            })
773            .unwrap_or(0)
774    }
775
776    pub async fn get_messages(&self, from: &Bech32Address) -> Result<Vec<Message>> {
777        let mut messages = Vec::new();
778        let mut cursor = None;
779
780        loop {
781            let response = self
782                .uncached_client()
783                .messages(
784                    Some(&from.into()),
785                    PaginationRequest {
786                        cursor: cursor.clone(),
787                        results: NUM_RESULTS_PER_REQUEST,
788                        direction: PageDirection::Forward,
789                    },
790                )
791                .await?;
792
793            if response.results.is_empty() {
794                break;
795            }
796
797            messages.extend(response.results.into_iter().map(Into::into));
798            cursor = response.cursor;
799        }
800
801        Ok(messages)
802    }
803
804    pub async fn get_message_proof(
805        &self,
806        tx_id: &TxId,
807        nonce: &Nonce,
808        commit_block_id: Option<&Bytes32>,
809        commit_block_height: Option<u32>,
810    ) -> Result<MessageProof> {
811        self.uncached_client()
812            .message_proof(
813                tx_id,
814                nonce,
815                commit_block_id.map(Into::into),
816                commit_block_height.map(Into::into),
817            )
818            .await
819            .map(Into::into)
820            .map_err(Into::into)
821    }
822
823    pub async fn is_user_account(&self, address: impl Into<Bytes32>) -> Result<bool> {
824        self.uncached_client()
825            .is_user_account(*address.into())
826            .await
827    }
828
829    pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
830        self.uncached_client_mut().set_retry_config(retry_config);
831
832        self
833    }
834
835    pub async fn contract_exists(&self, contract_id: &Bech32ContractId) -> Result<bool> {
836        Ok(self
837            .uncached_client()
838            .contract_exists(&contract_id.into())
839            .await?)
840    }
841
842    fn uncached_client(&self) -> &RetryableClient {
843        self.cached_client.inner()
844    }
845
846    fn uncached_client_mut(&mut self) -> &mut RetryableClient {
847        self.cached_client.inner_mut()
848    }
849}
850
851#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
852impl DryRunner for Provider {
853    async fn dry_run(&self, tx: FuelTransaction) -> Result<DryRun> {
854        let [tx_execution_status] = self
855            .uncached_client()
856            .dry_run_opt(&vec![tx], Some(false), Some(0))
857            .await?
858            .try_into()
859            .expect("should have only one element");
860
861        let receipts = tx_execution_status.result.receipts();
862        let script_gas = self.get_script_gas_used(receipts);
863
864        let variable_outputs = receipts
865            .iter()
866            .filter(
867                |receipt| matches!(receipt, Receipt::TransferOut { amount, .. } if *amount != 0),
868            )
869            .count();
870
871        let succeeded = matches!(
872            tx_execution_status.result,
873            TransactionExecutionResult::Success { .. }
874        );
875
876        let dry_run = DryRun {
877            succeeded,
878            script_gas,
879            variable_outputs,
880        };
881
882        Ok(dry_run)
883    }
884
885    async fn estimate_gas_price(&self, block_horizon: u32) -> Result<u64> {
886        Ok(self.estimate_gas_price(block_horizon).await?.gas_price)
887    }
888
889    async fn estimate_predicates(
890        &self,
891        tx: &FuelTransaction,
892        _latest_chain_executor_version: Option<u32>,
893    ) -> Result<FuelTransaction> {
894        Ok(self.uncached_client().estimate_predicates(tx).await?)
895    }
896
897    async fn consensus_parameters(&self) -> Result<ConsensusParameters> {
898        Provider::consensus_parameters(self).await
899    }
900}