fuels_signers/
provider.rs

1use std::{collections::HashMap, io};
2
3use chrono::{DateTime, Duration, Utc};
4#[cfg(feature = "fuel-core")]
5use fuel_core::service::{Config, FuelService};
6use fuel_core_client::client::{
7    schema::{
8        balance::Balance, block::TimeParameters as FuelTimeParameters, contract::ContractBalance,
9    },
10    types::TransactionStatus,
11    FuelClient, PageDirection, PaginatedResult, PaginationRequest,
12};
13use fuel_tx::{AssetId, ConsensusParameters, Input, Receipt, TxPointer, UtxoId};
14use fuel_types::MessageId;
15use fuel_vm::state::ProgramState;
16use fuels_types::{
17    bech32::{Bech32Address, Bech32ContractId},
18    block::Block,
19    chain_info::ChainInfo,
20    coin::Coin,
21    constants::{BASE_ASSET_ID, DEFAULT_GAS_ESTIMATION_TOLERANCE, MAX_GAS_PER_TX},
22    errors::{error, Error, Result},
23    message::Message,
24    message_proof::MessageProof,
25    node_info::NodeInfo,
26    resource::Resource,
27    transaction::Transaction,
28    transaction_response::TransactionResponse,
29};
30use itertools::Itertools;
31use tai64::Tai64;
32use thiserror::Error;
33
34type ProviderResult<T> = std::result::Result<T, ProviderError>;
35
36#[derive(Debug)]
37pub struct TransactionCost {
38    pub min_gas_price: u64,
39    pub gas_price: u64,
40    pub gas_used: u64,
41    pub metered_bytes_size: u64,
42    pub total_fee: u64,
43}
44
45#[derive(Debug)]
46// ANCHOR: time_parameters
47pub struct TimeParameters {
48    // The time to set on the first block
49    pub start_time: DateTime<Utc>,
50    // The time interval between subsequent blocks
51    pub block_time_interval: Duration,
52}
53// ANCHOR_END: time_parameters
54
55impl From<TimeParameters> for FuelTimeParameters {
56    fn from(time: TimeParameters) -> Self {
57        Self {
58            start_time: Tai64::from_unix(time.start_time.timestamp()).0.into(),
59            block_time_interval: (time.block_time_interval.num_seconds() as u64).into(),
60        }
61    }
62}
63
64pub(crate) struct ResourceQueries {
65    utxos: Vec<String>,
66    messages: Vec<String>,
67    asset_id: String,
68    amount: u64,
69}
70
71impl ResourceQueries {
72    pub fn new(
73        utxo_ids: Vec<UtxoId>,
74        message_ids: Vec<MessageId>,
75        asset_id: AssetId,
76        amount: u64,
77    ) -> Self {
78        let utxos = utxo_ids
79            .iter()
80            .map(|utxo_id| format!("{utxo_id:#x}"))
81            .collect::<Vec<_>>();
82
83        let messages = message_ids
84            .iter()
85            .map(|msg_id| format!("{msg_id:#x}"))
86            .collect::<Vec<_>>();
87
88        Self {
89            utxos,
90            messages,
91            asset_id: format!("{asset_id:#x}"),
92            amount,
93        }
94    }
95
96    pub fn exclusion_query(&self) -> Option<(Vec<&str>, Vec<&str>)> {
97        if self.utxos.is_empty() && self.messages.is_empty() {
98            return None;
99        }
100
101        let utxos_as_str = self.utxos.iter().map(AsRef::as_ref).collect::<Vec<_>>();
102
103        let msg_ids_as_str = self.messages.iter().map(AsRef::as_ref).collect::<Vec<_>>();
104
105        Some((utxos_as_str, msg_ids_as_str))
106    }
107
108    pub fn spend_query(&self) -> Vec<(&str, u64, Option<u64>)> {
109        vec![(self.asset_id.as_str(), self.amount, None)]
110    }
111}
112
113// ANCHOR: resource_filter
114pub struct ResourceFilter {
115    pub from: Bech32Address,
116    pub asset_id: AssetId,
117    pub amount: u64,
118    pub excluded_utxos: Vec<UtxoId>,
119    pub excluded_message_ids: Vec<MessageId>,
120}
121// ANCHOR_END: resource_filter
122
123impl ResourceFilter {
124    pub fn owner(&self) -> String {
125        self.from.hash().to_string()
126    }
127
128    pub(crate) fn resource_queries(&self) -> ResourceQueries {
129        ResourceQueries::new(
130            self.excluded_utxos.clone(),
131            self.excluded_message_ids.clone(),
132            self.asset_id,
133            self.amount,
134        )
135    }
136}
137
138impl Default for ResourceFilter {
139    fn default() -> Self {
140        Self {
141            from: Default::default(),
142            asset_id: BASE_ASSET_ID,
143            amount: Default::default(),
144            excluded_utxos: Default::default(),
145            excluded_message_ids: Default::default(),
146        }
147    }
148}
149
150#[derive(Debug, Error)]
151pub enum ProviderError {
152    // Every IO error in the context of Provider comes from the gql client
153    #[error(transparent)]
154    ClientRequestError(#[from] io::Error),
155}
156
157impl From<ProviderError> for Error {
158    fn from(e: ProviderError) -> Self {
159        Error::ProviderError(e.to_string())
160    }
161}
162
163/// Encapsulates common client operations in the SDK.
164/// Note that you may also use `client`, which is an instance
165/// of `FuelClient`, directly, which provides a broader API.
166#[derive(Debug, Clone)]
167pub struct Provider {
168    pub client: FuelClient,
169}
170
171impl Provider {
172    pub fn new(client: FuelClient) -> Self {
173        Self { client }
174    }
175
176    /// Sends a transaction to the underlying Provider's client.
177    pub async fn send_transaction<T: Transaction + Clone>(&self, tx: &T) -> Result<Vec<Receipt>> {
178        let tolerance = 0.0;
179        let TransactionCost {
180            gas_used,
181            min_gas_price,
182            ..
183        } = self.estimate_transaction_cost(tx, Some(tolerance)).await?;
184
185        if gas_used > tx.gas_limit() {
186            return Err(error!(
187                ProviderError,
188                "gas_limit({}) is lower than the estimated gas_used({})",
189                tx.gas_limit(),
190                gas_used
191            ));
192        } else if min_gas_price > tx.gas_price() {
193            return Err(error!(
194                ProviderError,
195                "gas_price({}) is lower than the required min_gas_price({})",
196                tx.gas_price(),
197                min_gas_price
198            ));
199        }
200
201        let chain_info = self.chain_info().await?;
202        tx.check_without_signatures(
203            chain_info.latest_block.header.height,
204            &chain_info.consensus_parameters,
205        )?;
206
207        let (status, receipts) = self.submit_with_feedback(tx.clone()).await?;
208        Self::if_failure_generate_error(&status, &receipts)?;
209
210        Ok(receipts)
211    }
212
213    fn if_failure_generate_error(status: &TransactionStatus, receipts: &[Receipt]) -> Result<()> {
214        if let TransactionStatus::Failure {
215            reason,
216            program_state,
217            ..
218        } = status
219        {
220            let revert_id = program_state
221                .and_then(|state| match state {
222                    ProgramState::Revert(revert_id) => Some(revert_id),
223                    _ => None,
224                })
225                .expect("Transaction failed without a `revert_id`");
226
227            return Err(Error::RevertTransactionError {
228                reason: reason.to_string(),
229                revert_id,
230                receipts: receipts.to_owned(),
231            });
232        }
233
234        Ok(())
235    }
236
237    async fn submit_with_feedback(
238        &self,
239        tx: impl Transaction,
240    ) -> ProviderResult<(TransactionStatus, Vec<Receipt>)> {
241        let tx_id = tx.id().to_string();
242        let status = self.client.submit_and_await_commit(&tx.into()).await?;
243        let receipts = self.client.receipts(&tx_id).await?;
244
245        Ok((status, receipts))
246    }
247
248    #[cfg(feature = "fuel-core")]
249    /// Launches a local `fuel-core` network based on provided config.
250    pub async fn launch(config: Config) -> Result<FuelClient> {
251        let srv = FuelService::new_node(config).await.unwrap();
252        Ok(FuelClient::from(srv.bound_address))
253    }
254
255    /// Connects to an existing node at the given address.
256    pub async fn connect(url: impl AsRef<str>) -> Result<Provider> {
257        let client = FuelClient::new(url).map_err(|err| error!(InfrastructureError, "{err}"))?;
258        Ok(Provider::new(client))
259    }
260
261    pub async fn chain_info(&self) -> ProviderResult<ChainInfo> {
262        Ok(self.client.chain_info().await?.into())
263    }
264
265    pub async fn consensus_parameters(&self) -> ProviderResult<ConsensusParameters> {
266        Ok(self.client.chain_info().await?.consensus_parameters.into())
267    }
268
269    pub async fn node_info(&self) -> ProviderResult<NodeInfo> {
270        Ok(self.client.node_info().await?.into())
271    }
272
273    pub async fn dry_run<T: Transaction + Clone>(&self, tx: &T) -> Result<Vec<Receipt>> {
274        let receipts = self.client.dry_run(&tx.clone().into()).await?;
275
276        Ok(receipts)
277    }
278
279    pub async fn dry_run_no_validation<T: Transaction + Clone>(
280        &self,
281        tx: &T,
282    ) -> Result<Vec<Receipt>> {
283        let receipts = self
284            .client
285            .dry_run_opt(&tx.clone().into(), Some(false))
286            .await?;
287
288        Ok(receipts)
289    }
290
291    /// Gets all unspent coins owned by address `from`, with asset ID `asset_id`.
292    pub async fn get_coins(
293        &self,
294        from: &Bech32Address,
295        asset_id: AssetId,
296    ) -> ProviderResult<Vec<Coin>> {
297        let mut coins: Vec<Coin> = vec![];
298
299        let mut cursor = None;
300
301        loop {
302            let res = self
303                .client
304                .coins(
305                    &from.hash().to_string(),
306                    Some(&asset_id.to_string()),
307                    PaginationRequest {
308                        cursor: cursor.clone(),
309                        results: 100,
310                        direction: PageDirection::Forward,
311                    },
312                )
313                .await?;
314
315            if res.results.is_empty() {
316                break;
317            }
318            coins.extend(res.results.into_iter().map(Into::into));
319            cursor = res.cursor;
320        }
321
322        Ok(coins)
323    }
324
325    /// Get some spendable coins of asset `asset_id` for address `from` that add up at least to
326    /// amount `amount`. The returned coins (UTXOs) are actual coins that can be spent. The number
327    /// of coins (UXTOs) is optimized to prevent dust accumulation.
328    pub async fn get_spendable_resources(
329        &self,
330        filter: ResourceFilter,
331    ) -> ProviderResult<Vec<Resource>> {
332        let queries = filter.resource_queries();
333
334        let res = self
335            .client
336            .resources_to_spend(
337                &filter.owner(),
338                queries.spend_query(),
339                queries.exclusion_query(),
340            )
341            .await?
342            .into_iter()
343            .flatten()
344            .map(|resource| {
345                resource
346                    .try_into()
347                    .map_err(ProviderError::ClientRequestError)
348            })
349            .try_collect()?;
350
351        Ok(res)
352    }
353
354    /// Returns a vector consisting of `Input::Coin`s and `Input::Message`s for the given
355    /// `ResourceFilter`. The `witness_index` is the position of the witness (signature)
356    /// in the transaction's list of witnesses. In the validation process, the node will
357    /// use the witness at this index to validate the coins returned by this method.
358    pub async fn get_asset_inputs(
359        &self,
360        filter: ResourceFilter,
361        witness_index: u8,
362    ) -> Result<Vec<Input>> {
363        let asset_id = filter.asset_id;
364        Ok(self
365            .get_spendable_resources(filter)
366            .await?
367            .iter()
368            .map(|resource| match resource {
369                Resource::Coin(coin) => self.create_coin_input(coin, asset_id, witness_index),
370                Resource::Message(message) => self.create_message_input(message, witness_index),
371            })
372            .collect::<Vec<Input>>())
373    }
374
375    fn create_coin_input(&self, coin: &Coin, asset_id: AssetId, witness_index: u8) -> Input {
376        Input::coin_signed(
377            coin.utxo_id,
378            coin.owner.clone().into(),
379            coin.amount,
380            asset_id,
381            TxPointer::default(),
382            witness_index,
383            0,
384        )
385    }
386
387    fn create_message_input(&self, message: &Message, witness_index: u8) -> Input {
388        Input::message_signed(
389            message.message_id(),
390            message.sender.clone().into(),
391            message.recipient.clone().into(),
392            message.amount,
393            message.nonce,
394            witness_index,
395            message.data.clone(),
396        )
397    }
398
399    /// Get the balance of all spendable coins `asset_id` for address `address`. This is different
400    /// from getting coins because we are just returning a number (the sum of UTXOs amount) instead
401    /// of the UTXOs.
402    pub async fn get_asset_balance(
403        &self,
404        address: &Bech32Address,
405        asset_id: AssetId,
406    ) -> ProviderResult<u64> {
407        self.client
408            .balance(&address.hash().to_string(), Some(&*asset_id.to_string()))
409            .await
410            .map_err(Into::into)
411    }
412
413    /// Get the balance of all spendable coins `asset_id` for contract with id `contract_id`.
414    pub async fn get_contract_asset_balance(
415        &self,
416        contract_id: &Bech32ContractId,
417        asset_id: AssetId,
418    ) -> ProviderResult<u64> {
419        self.client
420            .contract_balance(&contract_id.hash().to_string(), Some(&asset_id.to_string()))
421            .await
422            .map_err(Into::into)
423    }
424
425    /// Get all the spendable balances of all assets for address `address`. This is different from
426    /// getting the coins because we are only returning the numbers (the sum of UTXOs coins amount
427    /// for each asset id) and not the UTXOs coins themselves
428    pub async fn get_balances(
429        &self,
430        address: &Bech32Address,
431    ) -> ProviderResult<HashMap<String, u64>> {
432        // We don't paginate results because there are likely at most ~100 different assets in one
433        // wallet
434        let pagination = PaginationRequest {
435            cursor: None,
436            results: 9999,
437            direction: PageDirection::Forward,
438        };
439        let balances_vec = self
440            .client
441            .balances(&address.hash().to_string(), pagination)
442            .await?
443            .results;
444        let balances = balances_vec
445            .into_iter()
446            .map(
447                |Balance {
448                     owner: _,
449                     amount,
450                     asset_id,
451                 }| (asset_id.to_string(), amount.try_into().unwrap()),
452            )
453            .collect();
454        Ok(balances)
455    }
456
457    /// Get all balances of all assets for the contract with id `contract_id`.
458    pub async fn get_contract_balances(
459        &self,
460        contract_id: &Bech32ContractId,
461    ) -> ProviderResult<HashMap<String, u64>> {
462        // We don't paginate results because there are likely at most ~100 different assets in one
463        // wallet
464        let pagination = PaginationRequest {
465            cursor: None,
466            results: 9999,
467            direction: PageDirection::Forward,
468        };
469
470        let balances_vec = self
471            .client
472            .contract_balances(&contract_id.hash().to_string(), pagination)
473            .await?
474            .results;
475        let balances = balances_vec
476            .into_iter()
477            .map(
478                |ContractBalance {
479                     contract: _,
480                     amount,
481                     asset_id,
482                 }| (asset_id.to_string(), amount.try_into().unwrap()),
483            )
484            .collect();
485        Ok(balances)
486    }
487
488    pub async fn get_transaction_by_id(
489        &self,
490        tx_id: &str,
491    ) -> ProviderResult<Option<TransactionResponse>> {
492        Ok(self.client.transaction(tx_id).await?.map(Into::into))
493    }
494
495    // - Get transaction(s)
496    pub async fn get_transactions(
497        &self,
498        request: PaginationRequest<String>,
499    ) -> ProviderResult<PaginatedResult<TransactionResponse, String>> {
500        let pr = self.client.transactions(request).await?;
501
502        Ok(PaginatedResult {
503            cursor: pr.cursor,
504            results: pr.results.into_iter().map(Into::into).collect(),
505            has_next_page: pr.has_next_page,
506            has_previous_page: pr.has_previous_page,
507        })
508    }
509
510    // Get transaction(s) by owner
511    pub async fn get_transactions_by_owner(
512        &self,
513        owner: &Bech32Address,
514        request: PaginationRequest<String>,
515    ) -> ProviderResult<PaginatedResult<TransactionResponse, String>> {
516        let pr = self
517            .client
518            .transactions_by_owner(&owner.hash().to_string(), request)
519            .await?;
520
521        Ok(PaginatedResult {
522            cursor: pr.cursor,
523            results: pr.results.into_iter().map(Into::into).collect(),
524            has_next_page: pr.has_next_page,
525            has_previous_page: pr.has_previous_page,
526        })
527    }
528
529    pub async fn latest_block_height(&self) -> ProviderResult<u64> {
530        Ok(self.chain_info().await?.latest_block.header.height)
531    }
532
533    pub async fn latest_block_time(&self) -> ProviderResult<Option<DateTime<Utc>>> {
534        Ok(self.chain_info().await?.latest_block.header.time)
535    }
536
537    pub async fn produce_blocks(
538        &self,
539        amount: u64,
540        time: Option<TimeParameters>,
541    ) -> io::Result<u64> {
542        let fuel_time: Option<FuelTimeParameters> = time.map(|t| t.into());
543        self.client.produce_blocks(amount, fuel_time).await
544    }
545
546    /// Get block by id.
547    pub async fn block(&self, block_id: &str) -> ProviderResult<Option<Block>> {
548        let block = self.client.block(block_id).await?.map(Into::into);
549        Ok(block)
550    }
551
552    // - Get block(s)
553    pub async fn get_blocks(
554        &self,
555        request: PaginationRequest<String>,
556    ) -> ProviderResult<PaginatedResult<Block, String>> {
557        let pr = self.client.blocks(request).await?;
558
559        Ok(PaginatedResult {
560            cursor: pr.cursor,
561            results: pr.results.into_iter().map(Into::into).collect(),
562            has_next_page: pr.has_next_page,
563            has_previous_page: pr.has_previous_page,
564        })
565    }
566
567    pub async fn estimate_transaction_cost<T: Transaction + Clone>(
568        &self,
569        tx: &T,
570        tolerance: Option<f64>,
571    ) -> Result<TransactionCost> {
572        let NodeInfo { min_gas_price, .. } = self.node_info().await?;
573
574        let tolerance = tolerance.unwrap_or(DEFAULT_GAS_ESTIMATION_TOLERANCE);
575        let dry_run_tx = Self::generate_dry_run_tx(tx);
576        let consensus_parameters = self.chain_info().await?.consensus_parameters;
577        let gas_used = self
578            .get_gas_used_with_tolerance(&dry_run_tx, tolerance)
579            .await?;
580        let gas_price = std::cmp::max(tx.gas_price(), min_gas_price);
581
582        // Update the dry_run_tx with estimated gas_used and correct gas price to calculate the total_fee
583        dry_run_tx
584            .with_gas_price(gas_price)
585            .with_gas_limit(gas_used);
586
587        let transaction_fee = tx
588            .fee_checked_from_tx(&consensus_parameters)
589            .expect("Error calculating TransactionFee");
590
591        Ok(TransactionCost {
592            min_gas_price,
593            gas_price,
594            gas_used,
595            metered_bytes_size: tx.metered_bytes_size() as u64,
596            total_fee: transaction_fee.total(),
597        })
598    }
599
600    // Remove limits from an existing Transaction to get an accurate gas estimation
601    fn generate_dry_run_tx<T: Transaction + Clone>(tx: &T) -> T {
602        // Simulate the contract call with MAX_GAS_PER_TX to get the complete gas_used
603        tx.clone().with_gas_limit(MAX_GAS_PER_TX).with_gas_price(0)
604    }
605
606    // Increase estimated gas by the provided tolerance
607    async fn get_gas_used_with_tolerance<T: Transaction + Clone>(
608        &self,
609        tx: &T,
610        tolerance: f64,
611    ) -> Result<u64> {
612        let gas_used = self.get_gas_used(&self.dry_run_no_validation(tx).await?);
613        Ok((gas_used as f64 * (1.0 + tolerance)) as u64)
614    }
615
616    fn get_gas_used(&self, receipts: &[Receipt]) -> u64 {
617        receipts
618            .iter()
619            .rfind(|r| matches!(r, Receipt::ScriptResult { .. }))
620            .map(|script_result| {
621                script_result
622                    .gas_used()
623                    .expect("could not retrieve gas used from ScriptResult")
624            })
625            .unwrap_or(0)
626    }
627
628    pub async fn get_messages(&self, from: &Bech32Address) -> ProviderResult<Vec<Message>> {
629        let pagination = PaginationRequest {
630            cursor: None,
631            results: 100,
632            direction: PageDirection::Forward,
633        };
634        let res = self
635            .client
636            .messages(Some(&from.hash().to_string()), pagination)
637            .await?
638            .results
639            .into_iter()
640            .map(Into::into)
641            .collect();
642        Ok(res)
643    }
644
645    pub async fn get_message_proof(
646        &self,
647        tx_id: &str,
648        message_id: &str,
649    ) -> ProviderResult<Option<MessageProof>> {
650        let proof = self
651            .client
652            .message_proof(tx_id, message_id)
653            .await?
654            .map(Into::into);
655        Ok(proof)
656    }
657}