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::{provider::TransactionCost, Account};
6use fuels_core::{
7    codec::{ABIEncoder, DecoderConfig, EncoderConfig, LogDecoder},
8    traits::{Parameterize, Signer, Tokenizable},
9    types::{
10        bech32::{Bech32Address, Bech32ContractId},
11        errors::{error, transaction::Reason, Error, Result},
12        input::Input,
13        output::Output,
14        transaction::{ScriptTransaction, Transaction, TxPolicies},
15        transaction_builders::{
16            BuildableTransaction, ScriptBuildStrategy, ScriptTransactionBuilder,
17            TransactionBuilder, VariableOutputPolicy,
18        },
19        tx_status::TxStatus,
20        Selector, Token,
21    },
22};
23
24use crate::{
25    calls::{
26        receipt_parser::ReceiptParser,
27        traits::{ContractDependencyConfigurator, ResponseParser, TransactionTuner},
28        utils::find_id_of_missing_contract,
29        CallParameters, ContractCall, Execution, ScriptCall,
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(&mut self, execution: Execution) -> Result<CallResponse<T>> {
203        let provider = self.account.try_provider()?;
204
205        let tx_status = if let Execution::StateReadOnly = execution {
206            let tx = self
207                .transaction_builder()
208                .await?
209                .with_build_strategy(ScriptBuildStrategy::StateReadOnly)
210                .build(provider)
211                .await?;
212
213            provider.dry_run_opt(tx, false, Some(0)).await?
214        } else {
215            let tx = self.build_tx().await?;
216            provider.dry_run(tx).await?
217        };
218
219        self.get_response(tx_status)
220    }
221
222    /// Create a [`CallResponse`] from `TxStatus`
223    pub fn get_response(&self, tx_status: TxStatus) -> Result<CallResponse<T>> {
224        let success = tx_status.take_success_checked(Some(&self.log_decoder))?;
225
226        let token =
227            self.call
228                .parse_call(&success.receipts, self.decoder_config, &T::param_type())?;
229
230        Ok(CallResponse {
231            value: T::from_token(token)?,
232            log_decoder: self.log_decoder.clone(),
233            tx_id: self.cached_tx_id,
234            tx_status: success,
235        })
236    }
237
238    pub async fn determine_missing_contracts(mut self, max_attempts: Option<u64>) -> Result<Self> {
239        let attempts = max_attempts.unwrap_or(10);
240
241        for _ in 0..attempts {
242            match self.simulate(Execution::Realistic).await {
243                Ok(_) => return Ok(self),
244
245                Err(Error::Transaction(Reason::Reverted { ref receipts, .. })) => {
246                    if let Some(contract_id) = find_id_of_missing_contract(receipts) {
247                        self.call.append_external_contract(contract_id);
248                    }
249                }
250
251                Err(other_error) => return Err(other_error),
252            }
253        }
254
255        self.simulate(Execution::Realistic).await.map(|_| self)
256    }
257}
258
259impl<A, T> CallHandler<A, ContractCall, T>
260where
261    A: Account,
262    T: Tokenizable + Parameterize + Debug,
263{
264    pub fn new_contract_call(
265        contract_id: Bech32ContractId,
266        account: A,
267        encoded_selector: Selector,
268        args: &[Token],
269        log_decoder: LogDecoder,
270        is_payable: bool,
271        encoder_config: EncoderConfig,
272    ) -> Self {
273        let call = ContractCall {
274            contract_id,
275            encoded_selector,
276            encoded_args: ABIEncoder::new(encoder_config).encode(args),
277            call_parameters: CallParameters::default(),
278            external_contracts: vec![],
279            output_param: T::param_type(),
280            is_payable,
281            custom_assets: Default::default(),
282            inputs: vec![],
283            outputs: vec![],
284        };
285        CallHandler {
286            account,
287            call,
288            tx_policies: TxPolicies::default(),
289            log_decoder,
290            datatype: PhantomData,
291            decoder_config: DecoderConfig::default(),
292            cached_tx_id: None,
293            variable_output_policy: VariableOutputPolicy::default(),
294            unresolved_signers: vec![],
295        }
296    }
297
298    /// Adds a custom `asset_id` with its `amount` and an optional `address` to be used for
299    /// generating outputs to this contract's call.
300    ///
301    /// # Parameters
302    /// - `asset_id`: The unique identifier of the asset being added.
303    /// - `amount`: The amount of the asset being added.
304    /// - `address`: The optional account address that the output amount will be sent to.
305    ///              If not provided, the asset will be sent to the users account address.
306    ///
307    /// Note that this is a builder method, i.e. use it as a chain:
308    ///
309    /// ```ignore
310    /// let asset_id = AssetId::from([3u8; 32]);
311    /// let amount = 5000;
312    /// my_contract_instance.my_method(...).add_custom_asset(asset_id, amount, None).call()
313    /// ```
314    pub fn add_custom_asset(
315        mut self,
316        asset_id: AssetId,
317        amount: u64,
318        to: Option<Bech32Address>,
319    ) -> Self {
320        self.call.add_custom_asset(asset_id, amount, to);
321        self
322    }
323
324    pub fn is_payable(&self) -> bool {
325        self.call.is_payable
326    }
327
328    /// Sets the call parameters for a given contract call.
329    /// Note that this is a builder method, i.e. use it as a chain:
330    ///
331    /// ```ignore
332    /// let params = CallParameters { amount: 1, asset_id: AssetId::zeroed() };
333    /// my_contract_instance.my_method(...).call_params(params).call()
334    /// ```
335    pub fn call_params(mut self, params: CallParameters) -> Result<Self> {
336        if !self.is_payable() && params.amount() > 0 {
337            return Err(error!(Other, "assets forwarded to non-payable method"));
338        }
339        self.call.call_parameters = params;
340
341        Ok(self)
342    }
343
344    /// Add custom outputs to the `CallHandler`. These outputs
345    /// will appear at the **start** of the final output list.
346    pub fn with_outputs(mut self, outputs: Vec<Output>) -> Self {
347        self.call = self.call.with_outputs(outputs);
348        self
349    }
350
351    /// Add custom inputs to the `CallHandler`. These inputs
352    /// will appear at the **start** of the final input list.
353    pub fn with_inputs(mut self, inputs: Vec<Input>) -> Self {
354        self.call = self.call.with_inputs(inputs);
355        self
356    }
357}
358
359impl<A, T> CallHandler<A, ScriptCall, T>
360where
361    A: Account,
362    T: Parameterize + Tokenizable + Debug,
363{
364    pub fn new_script_call(
365        script_binary: Vec<u8>,
366        encoded_args: Result<Vec<u8>>,
367        account: A,
368        log_decoder: LogDecoder,
369    ) -> Self {
370        let call = ScriptCall {
371            script_binary,
372            encoded_args,
373            inputs: vec![],
374            outputs: vec![],
375            external_contracts: vec![],
376        };
377
378        Self {
379            account,
380            call,
381            tx_policies: TxPolicies::default(),
382            log_decoder,
383            datatype: PhantomData,
384            decoder_config: DecoderConfig::default(),
385            cached_tx_id: None,
386            variable_output_policy: VariableOutputPolicy::default(),
387            unresolved_signers: vec![],
388        }
389    }
390
391    /// Add custom outputs to the `CallHandler`. These outputs
392    /// will appear at the **start** of the final output list.
393    pub fn with_outputs(mut self, outputs: Vec<Output>) -> Self {
394        self.call = self.call.with_outputs(outputs);
395        self
396    }
397
398    /// Add custom inputs to the `CallHandler`. These inputs
399    /// will appear at the **start** of the final input list.
400    pub fn with_inputs(mut self, inputs: Vec<Input>) -> Self {
401        self.call = self.call.with_inputs(inputs);
402        self
403    }
404}
405
406impl<A> CallHandler<A, Vec<ContractCall>, ()>
407where
408    A: Account,
409{
410    pub fn new_multi_call(account: A) -> Self {
411        Self {
412            account,
413            call: vec![],
414            tx_policies: TxPolicies::default(),
415            log_decoder: LogDecoder::new(Default::default()),
416            datatype: PhantomData,
417            decoder_config: DecoderConfig::default(),
418            cached_tx_id: None,
419            variable_output_policy: VariableOutputPolicy::default(),
420            unresolved_signers: vec![],
421        }
422    }
423
424    fn append_external_contract(mut self, contract_id: Bech32ContractId) -> Result<Self> {
425        if self.call.is_empty() {
426            return Err(error!(
427                Other,
428                "no calls added. Have you used '.add_calls()'?"
429            ));
430        }
431
432        self.call
433            .iter_mut()
434            .take(1)
435            .for_each(|call| call.append_external_contract(contract_id.clone()));
436
437        Ok(self)
438    }
439
440    /// Adds a contract call to be bundled in the transaction.
441    /// Note that if you added custom inputs/outputs that they will follow the
442    /// order in which the calls are added.
443    pub fn add_call(
444        mut self,
445        call_handler: CallHandler<impl Account, ContractCall, impl Tokenizable>,
446    ) -> Self {
447        self.log_decoder.merge(call_handler.log_decoder);
448        self.call.push(call_handler.call);
449        self.unresolved_signers
450            .extend(call_handler.unresolved_signers);
451
452        self
453    }
454
455    /// Call contract methods on the node, in a state-modifying manner.
456    pub async fn call<T: Tokenizable + Debug>(mut self) -> Result<CallResponse<T>> {
457        let tx = self.build_tx().await?;
458
459        let provider = self.account.try_provider()?;
460        let consensus_parameters = provider.consensus_parameters().await?;
461        let chain_id = consensus_parameters.chain_id();
462
463        self.cached_tx_id = Some(tx.id(chain_id));
464
465        let tx_status = provider.send_transaction_and_await_commit(tx).await?;
466
467        self.get_response(tx_status)
468    }
469
470    pub async fn submit(mut self) -> Result<SubmitResponse<A, Vec<ContractCall>, ()>> {
471        let tx = self.build_tx().await?;
472        let provider = self.account.try_provider()?;
473
474        let tx_id = provider.send_transaction(tx).await?;
475        self.cached_tx_id = Some(tx_id);
476
477        Ok(SubmitResponse::<A, Vec<ContractCall>, ()>::new(tx_id, self))
478    }
479
480    /// Call contract methods on the node, in a simulated manner, meaning the state of the
481    /// blockchain is *not* modified but simulated.
482    /// It is the same as the [call] method because the API is more user-friendly this way.
483    ///
484    /// [call]: Self::call
485    pub async fn simulate<T: Tokenizable + Debug>(
486        &mut self,
487        execution: Execution,
488    ) -> Result<CallResponse<T>> {
489        let provider = self.account.try_provider()?;
490
491        let tx_status = if let Execution::StateReadOnly = execution {
492            let tx = self
493                .transaction_builder()
494                .await?
495                .with_build_strategy(ScriptBuildStrategy::StateReadOnly)
496                .build(provider)
497                .await?;
498
499            provider.dry_run_opt(tx, false, Some(0)).await?
500        } else {
501            let tx = self.build_tx().await?;
502            provider.dry_run(tx).await?
503        };
504
505        self.get_response(tx_status)
506    }
507
508    /// Simulates a call without needing to resolve the generic for the return type
509    async fn simulate_without_decode(&self) -> Result<()> {
510        let provider = self.account.try_provider()?;
511        let tx = self.build_tx().await?;
512
513        provider.dry_run(tx).await?.check(None)?;
514
515        Ok(())
516    }
517
518    /// Create a [`CallResponse`] from `TxStatus`
519    pub fn get_response<T: Tokenizable + Debug>(
520        &self,
521        tx_status: TxStatus,
522    ) -> Result<CallResponse<T>> {
523        let success = tx_status.take_success_checked(Some(&self.log_decoder))?;
524        let mut receipt_parser = ReceiptParser::new(&success.receipts, self.decoder_config);
525
526        let final_tokens = self
527            .call
528            .iter()
529            .map(|call| receipt_parser.parse_call(&call.contract_id, &call.output_param))
530            .collect::<Result<Vec<_>>>()?;
531
532        let tokens_as_tuple = Token::Tuple(final_tokens);
533
534        Ok(CallResponse {
535            value: T::from_token(tokens_as_tuple)?,
536            log_decoder: self.log_decoder.clone(),
537            tx_id: self.cached_tx_id,
538            tx_status: success,
539        })
540    }
541
542    /// Simulates the call and attempts to resolve missing contract outputs.
543    /// Forwards the received error if it cannot be fixed.
544    pub async fn determine_missing_contracts(mut self, max_attempts: Option<u64>) -> Result<Self> {
545        let attempts = max_attempts.unwrap_or(10);
546
547        for _ in 0..attempts {
548            match self.simulate_without_decode().await {
549                Ok(_) => return Ok(self),
550
551                Err(Error::Transaction(Reason::Reverted { ref receipts, .. })) => {
552                    if let Some(contract_id) = find_id_of_missing_contract(receipts) {
553                        self = self.append_external_contract(contract_id)?;
554                    }
555                }
556
557                Err(other_error) => return Err(other_error),
558            }
559        }
560
561        self.simulate_without_decode().await.map(|_| self)
562    }
563}