fuels_programs/calls/
call_handler.rs

1use crate::{
2    calls::{
3        CallParameters, ContractCall, Execution, ExecutionType, ScriptCall,
4        receipt_parser::ReceiptParser,
5        traits::{ContractDependencyConfigurator, ResponseParser, TransactionTuner},
6        utils::find_ids_of_missing_contracts,
7    },
8    responses::{CallResponse, SubmitResponse},
9};
10use core::{fmt::Debug, marker::PhantomData};
11use fuel_tx::ConsensusParameters;
12use fuels_accounts::{Account, provider::TransactionCost};
13use fuels_core::{
14    codec::{ABIEncoder, DecoderConfig, EncoderConfig, LogDecoder},
15    traits::{Parameterize, Signer, Tokenizable},
16    types::{
17        Address, AssetId, Bytes32, ContractId, Selector, Token,
18        errors::{Error, Result, error, transaction::Reason},
19        input::Input,
20        output::Output,
21        transaction::{ScriptTransaction, Transaction, TxPolicies},
22        transaction_builders::{
23            BuildableTransaction, ScriptBuildStrategy, ScriptTransactionBuilder,
24            TransactionBuilder, VariableOutputPolicy,
25        },
26        tx_status::TxStatus,
27    },
28};
29use std::sync::Arc;
30
31// Trait implemented by contract instances so that
32// they can be passed to the `with_contracts` method
33pub trait ContractDependency {
34    fn id(&self) -> ContractId;
35    fn log_decoder(&self) -> LogDecoder;
36}
37
38#[derive(Debug, Clone)]
39#[must_use = "contract calls do nothing unless you `call` them"]
40/// Helper that handles submitting a call to a client and formatting the response
41pub struct CallHandler<A, C, T> {
42    pub account: A,
43    pub call: C,
44    pub tx_policies: TxPolicies,
45    pub log_decoder: LogDecoder,
46    pub datatype: PhantomData<T>,
47    decoder_config: DecoderConfig,
48    // Initially `None`, gets set to the right tx id after the transaction is submitted
49    cached_tx_id: Option<Bytes32>,
50    variable_output_policy: VariableOutputPolicy,
51    unresolved_signers: Vec<Arc<dyn Signer + Send + Sync>>,
52}
53
54impl<A, C, T> CallHandler<A, C, T> {
55    /// Sets the transaction policies for a given transaction.
56    /// Note that this is a builder method, i.e. use it as a chain:
57    /// ```ignore
58    /// let tx_policies = TxPolicies::default().with_gas_price(100);
59    /// my_contract_instance.my_method(...).with_tx_policies(tx_policies).call()
60    /// ```
61    pub fn with_tx_policies(mut self, tx_policies: TxPolicies) -> Self {
62        self.tx_policies = tx_policies;
63        self
64    }
65
66    pub fn with_decoder_config(mut self, decoder_config: DecoderConfig) -> Self {
67        self.decoder_config = decoder_config;
68        self.log_decoder.set_decoder_config(decoder_config);
69        self
70    }
71
72    /// If this method is not called, the default policy is to not add any variable outputs.
73    ///
74    /// # Parameters
75    /// - `variable_outputs`: The [`VariableOutputPolicy`] to apply for the contract call.
76    ///
77    /// # Returns
78    /// - `Self`: The updated SDK configuration.
79    pub fn with_variable_output_policy(mut self, variable_outputs: VariableOutputPolicy) -> Self {
80        self.variable_output_policy = variable_outputs;
81        self
82    }
83
84    pub fn add_signer(mut self, signer: impl Signer + Send + Sync + 'static) -> Self {
85        self.unresolved_signers.push(Arc::new(signer));
86        self
87    }
88}
89
90impl<A, C, T> CallHandler<A, C, T>
91where
92    A: Account,
93    C: TransactionTuner,
94    T: Tokenizable + Parameterize + Debug,
95{
96    pub async fn transaction_builder(&self) -> Result<ScriptTransactionBuilder> {
97        let consensus_parameters = self.account.try_provider()?.consensus_parameters().await?;
98        let required_asset_amounts = self
99            .call
100            .required_assets(*consensus_parameters.base_asset_id());
101
102        // Find the spendable resources required for those calls
103        let mut asset_inputs = vec![];
104        for &(asset_id, amount) in &required_asset_amounts {
105            let resources = self
106                .account
107                .get_asset_inputs_for_amount(asset_id, amount, None)
108                .await?;
109            asset_inputs.extend(resources);
110        }
111
112        self.transaction_builder_with_parameters(&consensus_parameters, asset_inputs)
113    }
114
115    pub fn transaction_builder_with_parameters(
116        &self,
117        consensus_parameters: &ConsensusParameters,
118        asset_inputs: Vec<Input>,
119    ) -> Result<ScriptTransactionBuilder> {
120        let mut tb = self.call.transaction_builder(
121            self.tx_policies,
122            self.variable_output_policy,
123            consensus_parameters,
124            asset_inputs,
125            &self.account,
126        )?;
127
128        tb.add_signers(&self.unresolved_signers)?;
129
130        Ok(tb)
131    }
132
133    /// Returns the script that executes the contract call
134    pub async fn build_tx(&self) -> Result<ScriptTransaction> {
135        let tb = self.transaction_builder().await?;
136
137        self.call.build_tx(tb, &self.account).await
138    }
139
140    /// Get a call's estimated cost
141    pub async fn estimate_transaction_cost(
142        &self,
143        tolerance: Option<f64>,
144        block_horizon: Option<u32>,
145    ) -> Result<TransactionCost> {
146        let tx = self.build_tx().await?;
147        let provider = self.account.try_provider()?;
148
149        let transaction_cost = provider
150            .estimate_transaction_cost(tx, tolerance, block_horizon)
151            .await?;
152
153        Ok(transaction_cost)
154    }
155}
156
157impl<A, C, T> CallHandler<A, C, T>
158where
159    A: Account,
160    C: ContractDependencyConfigurator + TransactionTuner + ResponseParser,
161    T: Tokenizable + Parameterize + Debug,
162{
163    /// Sets external contracts as dependencies to this contract's call.
164    /// Effectively, this will be used to create [`fuel_tx::Input::Contract`]/[`fuel_tx::Output::Contract`]
165    /// pairs and set them into the transaction. Note that this is a builder
166    /// method, i.e. use it as a chain:
167    ///
168    /// ```ignore
169    /// my_contract_instance.my_method(...).with_contract_ids(&[another_contract_id]).call()
170    /// ```
171    ///
172    /// [`Input::Contract`]: fuel_tx::Input::Contract
173    /// [`Output::Contract`]: fuel_tx::Output::Contract
174    pub fn with_contract_ids(mut self, contract_ids: &[ContractId]) -> Self {
175        self.call = self.call.with_external_contracts(contract_ids.to_vec());
176
177        self
178    }
179
180    /// Sets external contract instances as dependencies to this contract's call.
181    /// Effectively, this will be used to: merge `LogDecoder`s and create
182    /// [`fuel_tx::Input::Contract`]/[`fuel_tx::Output::Contract`] pairs and set them into the transaction.
183    /// Note that this is a builder method, i.e. use it as a chain:
184    ///
185    /// ```ignore
186    /// my_contract_instance.my_method(...).with_contracts(&[another_contract_instance]).call()
187    /// ```
188    pub fn with_contracts(mut self, contracts: &[&dyn ContractDependency]) -> Self {
189        self.call = self
190            .call
191            .with_external_contracts(contracts.iter().map(|c| c.id()).collect());
192        for c in contracts {
193            self.log_decoder.merge(c.log_decoder());
194        }
195
196        self
197    }
198
199    /// Call a contract's method on the node, in a state-modifying manner.
200    pub async fn call(mut self) -> Result<CallResponse<T>> {
201        let tx = self.build_tx().await?;
202        let provider = self.account.try_provider()?;
203
204        let consensus_parameters = provider.consensus_parameters().await?;
205        let chain_id = consensus_parameters.chain_id();
206        self.cached_tx_id = Some(tx.id(chain_id));
207
208        let tx_status = provider.send_transaction_and_await_commit(tx).await?;
209
210        self.get_response(tx_status)
211    }
212
213    pub async fn submit(mut self) -> Result<SubmitResponse<A, C, T>> {
214        let tx = self.build_tx().await?;
215        let provider = self.account.try_provider()?;
216
217        let tx_id = provider.send_transaction(tx.clone()).await?;
218        self.cached_tx_id = Some(tx_id);
219
220        Ok(SubmitResponse::<A, C, T>::new(tx_id, self))
221    }
222
223    /// Call a contract's method on the node, in a simulated manner, meaning the state of the
224    /// blockchain is *not* modified but simulated.
225    pub async fn simulate(
226        &mut self,
227        Execution {
228            execution_type,
229            at_height,
230        }: Execution,
231    ) -> Result<CallResponse<T>> {
232        let provider = self.account.try_provider()?;
233
234        let tx_status = if let ExecutionType::StateReadOnly = execution_type {
235            let tx = self
236                .transaction_builder()
237                .await?
238                .with_build_strategy(ScriptBuildStrategy::StateReadOnly)
239                .build(provider)
240                .await?;
241
242            provider.dry_run_opt(tx, false, Some(0), at_height).await?
243        } else {
244            let tx = self.build_tx().await?;
245            provider.dry_run_opt(tx, true, None, at_height).await?
246        };
247
248        self.get_response(tx_status)
249    }
250
251    /// Create a [`CallResponse`] from `TxStatus`
252    pub fn get_response(&self, tx_status: TxStatus) -> Result<CallResponse<T>> {
253        let success = tx_status.take_success_checked(Some(&self.log_decoder))?;
254
255        let token =
256            self.call
257                .parse_call(&success.receipts, self.decoder_config, &T::param_type())?;
258
259        Ok(CallResponse {
260            value: T::from_token(token)?,
261            log_decoder: self.log_decoder.clone(),
262            tx_id: self.cached_tx_id,
263            tx_status: success,
264        })
265    }
266
267    pub async fn determine_missing_contracts(mut self) -> Result<Self> {
268        match self.simulate(Execution::realistic()).await {
269            Ok(_) => Ok(self),
270
271            Err(Error::Transaction(Reason::Failure { ref receipts, .. })) => {
272                for contract_id in find_ids_of_missing_contracts(receipts) {
273                    self.call.append_external_contract(contract_id);
274                }
275
276                Ok(self)
277            }
278
279            Err(other_error) => Err(other_error),
280        }
281    }
282}
283
284impl<A, T> CallHandler<A, ContractCall, T>
285where
286    A: Account,
287    T: Tokenizable + Parameterize + Debug,
288{
289    pub fn new_contract_call(
290        contract_id: ContractId,
291        account: A,
292        encoded_selector: Selector,
293        args: &[Token],
294        log_decoder: LogDecoder,
295        is_payable: bool,
296        encoder_config: EncoderConfig,
297    ) -> Self {
298        let call = ContractCall {
299            contract_id,
300            encoded_selector,
301            encoded_args: ABIEncoder::new(encoder_config).encode(args),
302            call_parameters: CallParameters::default(),
303            external_contracts: vec![],
304            output_param: T::param_type(),
305            is_payable,
306            custom_assets: Default::default(),
307            inputs: vec![],
308            outputs: vec![],
309        };
310        CallHandler {
311            account,
312            call,
313            tx_policies: TxPolicies::default(),
314            log_decoder,
315            datatype: PhantomData,
316            decoder_config: DecoderConfig::default(),
317            cached_tx_id: None,
318            variable_output_policy: VariableOutputPolicy::default(),
319            unresolved_signers: vec![],
320        }
321    }
322
323    /// Adds a custom `asset_id` with its `amount` and an optional `address` to be used for
324    /// generating outputs to this contract's call.
325    ///
326    /// # Parameters
327    /// - `asset_id`: The unique identifier of the asset being added.
328    /// - `amount`: The amount of the asset being added.
329    /// - `address`: The optional account address that the output amount will be sent to.
330    ///   If not provided, the asset will be sent to the users account address.
331    ///
332    /// Note that this is a builder method, i.e. use it as a chain:
333    ///
334    /// ```ignore
335    /// let asset_id = AssetId::from([3u8; 32]);
336    /// let amount = 5000;
337    /// my_contract_instance.my_method(...).add_custom_asset(asset_id, amount, None).call()
338    /// ```
339    pub fn add_custom_asset(mut self, asset_id: AssetId, amount: u64, to: Option<Address>) -> Self {
340        self.call.add_custom_asset(asset_id, amount, to);
341        self
342    }
343
344    pub fn is_payable(&self) -> bool {
345        self.call.is_payable
346    }
347
348    /// Sets the call parameters for a given contract call.
349    /// Note that this is a builder method, i.e. use it as a chain:
350    ///
351    /// ```ignore
352    /// let params = CallParameters { amount: 1, asset_id: AssetId::zeroed() };
353    /// my_contract_instance.my_method(...).call_params(params).call()
354    /// ```
355    pub fn call_params(mut self, params: CallParameters) -> Result<Self> {
356        if !self.is_payable() && params.amount() > 0 {
357            return Err(error!(Other, "assets forwarded to non-payable method"));
358        }
359        self.call.call_parameters = params;
360
361        Ok(self)
362    }
363
364    /// Add custom outputs to the `CallHandler`. These outputs
365    /// will appear at the **start** of the final output list.
366    pub fn with_outputs(mut self, outputs: Vec<Output>) -> Self {
367        self.call = self.call.with_outputs(outputs);
368        self
369    }
370
371    /// Add custom inputs to the `CallHandler`. These inputs
372    /// will appear at the **start** of the final input list.
373    pub fn with_inputs(mut self, inputs: Vec<Input>) -> Self {
374        self.call = self.call.with_inputs(inputs);
375        self
376    }
377}
378
379impl<A, T> CallHandler<A, ScriptCall, T>
380where
381    A: Account,
382    T: Parameterize + Tokenizable + Debug,
383{
384    pub fn new_script_call(
385        script_binary: Vec<u8>,
386        encoded_args: Result<Vec<u8>>,
387        account: A,
388        log_decoder: LogDecoder,
389    ) -> Self {
390        let call = ScriptCall {
391            script_binary,
392            encoded_args,
393            inputs: vec![],
394            outputs: vec![],
395            external_contracts: vec![],
396        };
397
398        Self {
399            account,
400            call,
401            tx_policies: TxPolicies::default(),
402            log_decoder,
403            datatype: PhantomData,
404            decoder_config: DecoderConfig::default(),
405            cached_tx_id: None,
406            variable_output_policy: VariableOutputPolicy::default(),
407            unresolved_signers: vec![],
408        }
409    }
410
411    /// Add custom outputs to the `CallHandler`. These outputs
412    /// will appear at the **start** of the final output list.
413    pub fn with_outputs(mut self, outputs: Vec<Output>) -> Self {
414        self.call = self.call.with_outputs(outputs);
415        self
416    }
417
418    /// Add custom inputs to the `CallHandler`. These inputs
419    /// will appear at the **start** of the final input list.
420    pub fn with_inputs(mut self, inputs: Vec<Input>) -> Self {
421        self.call = self.call.with_inputs(inputs);
422        self
423    }
424}
425
426impl<A> CallHandler<A, Vec<ContractCall>, ()>
427where
428    A: Account,
429{
430    pub fn new_multi_call(account: A) -> Self {
431        Self {
432            account,
433            call: vec![],
434            tx_policies: TxPolicies::default(),
435            log_decoder: LogDecoder::new(Default::default(), Default::default()),
436            datatype: PhantomData,
437            decoder_config: DecoderConfig::default(),
438            cached_tx_id: None,
439            variable_output_policy: VariableOutputPolicy::default(),
440            unresolved_signers: vec![],
441        }
442    }
443
444    fn append_external_contract(mut self, contract_id: ContractId) -> Result<Self> {
445        if self.call.is_empty() {
446            return Err(error!(
447                Other,
448                "no calls added. Have you used '.add_calls()'?"
449            ));
450        }
451
452        self.call
453            .iter_mut()
454            .take(1)
455            .for_each(|call| call.append_external_contract(contract_id));
456
457        Ok(self)
458    }
459
460    /// Adds a contract call to be bundled in the transaction.
461    /// Note that if you added custom inputs/outputs that they will follow the
462    /// order in which the calls are added.
463    pub fn add_call(
464        mut self,
465        call_handler: CallHandler<impl Account, ContractCall, impl Tokenizable>,
466    ) -> Self {
467        self.log_decoder.merge(call_handler.log_decoder);
468        self.call.push(call_handler.call);
469        self.unresolved_signers
470            .extend(call_handler.unresolved_signers);
471
472        self
473    }
474
475    /// Call contract methods on the node, in a state-modifying manner.
476    pub async fn call<T: Tokenizable + Debug>(mut self) -> Result<CallResponse<T>> {
477        let tx = self.build_tx().await?;
478
479        let provider = self.account.try_provider()?;
480        let consensus_parameters = provider.consensus_parameters().await?;
481        let chain_id = consensus_parameters.chain_id();
482
483        self.cached_tx_id = Some(tx.id(chain_id));
484
485        let tx_status = provider.send_transaction_and_await_commit(tx).await?;
486
487        self.get_response(tx_status)
488    }
489
490    pub async fn submit(mut self) -> Result<SubmitResponse<A, Vec<ContractCall>, ()>> {
491        let tx = self.build_tx().await?;
492        let provider = self.account.try_provider()?;
493
494        let tx_id = provider.send_transaction(tx).await?;
495        self.cached_tx_id = Some(tx_id);
496
497        Ok(SubmitResponse::<A, Vec<ContractCall>, ()>::new(tx_id, self))
498    }
499
500    /// Call contract methods on the node, in a simulated manner, meaning the state of the
501    /// blockchain is *not* modified but simulated.
502    /// It is the same as the [call] method because the API is more user-friendly this way.
503    ///
504    /// [call]: Self::call
505    pub async fn simulate<T: Tokenizable + Debug>(
506        &mut self,
507        Execution {
508            execution_type,
509            at_height,
510        }: Execution,
511    ) -> Result<CallResponse<T>> {
512        let provider = self.account.try_provider()?;
513
514        let tx_status = if let ExecutionType::StateReadOnly = execution_type {
515            let tx = self
516                .transaction_builder()
517                .await?
518                .with_build_strategy(ScriptBuildStrategy::StateReadOnly)
519                .build(provider)
520                .await?;
521
522            provider.dry_run_opt(tx, false, Some(0), at_height).await?
523        } else {
524            let tx = self.build_tx().await?;
525            provider.dry_run_opt(tx, true, None, at_height).await?
526        };
527
528        self.get_response(tx_status)
529    }
530
531    /// Simulates a call without needing to resolve the generic for the return type
532    async fn simulate_without_decode(&self) -> Result<()> {
533        let provider = self.account.try_provider()?;
534        let tx = self.build_tx().await?;
535
536        provider.dry_run(tx).await?.check(None)?;
537
538        Ok(())
539    }
540
541    /// Create a [`CallResponse`] from `TxStatus`
542    pub fn get_response<T: Tokenizable + Debug>(
543        &self,
544        tx_status: TxStatus,
545    ) -> Result<CallResponse<T>> {
546        let success = tx_status.take_success_checked(Some(&self.log_decoder))?;
547        let mut receipt_parser = ReceiptParser::new(&success.receipts, self.decoder_config);
548
549        let final_tokens = self
550            .call
551            .iter()
552            .map(|call| receipt_parser.parse_call(call.contract_id, &call.output_param))
553            .collect::<Result<Vec<_>>>()?;
554
555        let tokens_as_tuple = Token::Tuple(final_tokens);
556
557        Ok(CallResponse {
558            value: T::from_token(tokens_as_tuple)?,
559            log_decoder: self.log_decoder.clone(),
560            tx_id: self.cached_tx_id,
561            tx_status: success,
562        })
563    }
564
565    /// Simulates the call and attempts to resolve missing contract outputs.
566    /// Forwards the received error if it cannot be fixed.
567    pub async fn determine_missing_contracts(mut self) -> Result<Self> {
568        match self.simulate_without_decode().await {
569            Ok(_) => Ok(self),
570
571            Err(Error::Transaction(Reason::Failure { ref receipts, .. })) => {
572                for contract_id in find_ids_of_missing_contracts(receipts) {
573                    self = self.append_external_contract(contract_id)?;
574                }
575
576                Ok(self)
577            }
578
579            Err(other_error) => Err(other_error),
580        }
581    }
582}