fuels_programs/calls/
call_handler.rs

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