fuels_programs/calls/
call_handler.rs

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